Add scene transitions (fade, wipe) to SceneManager
Switch(scene, transition) covers the old scene, swaps at full coverage (hiding slow OnLoad), then reveals the new one. Built-in Fade and Wipe transitions draw through TransitionRenderer (BasicEffect quad, no SpriteBatch); custom transitions subclass Transition. Runs on unscaled time so it works while gameplay is paused; Switch during a transition replaces the pending target. Sample: Tab now fades into the stress scene and wipes back. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
ff2231a8ab
commit
2b7d4c4fef
@@ -138,6 +138,19 @@ public static partial class GameAssets
|
|||||||
0 аллокаций на кадр. Контролируется бенчмарками (BenchmarkDotNet) и
|
0 аллокаций на кадр. Контролируется бенчмарками (BenchmarkDotNet) и
|
||||||
стресс-сценой в Sample.
|
стресс-сценой в Sample.
|
||||||
|
|
||||||
|
## Сцены и переходы
|
||||||
|
|
||||||
|
- `SceneManager` владеет активной сценой; обычное переключение откладывается до начала
|
||||||
|
следующего кадра (сцена никогда не выгружается посреди собственного кадра).
|
||||||
|
- `Scenes.Switch(scene, Transition.Fade(0.5f))` — переключение с визуальным переходом:
|
||||||
|
фаза закрытия (старая сцена живёт) → своп при полном покрытии → фаза открытия.
|
||||||
|
Тяжёлый `OnLoad` новой сцены скрыт за полностью закрытым экраном.
|
||||||
|
- Встроенные переходы: `Transition.Fade(duration, color)` и `Transition.Wipe(duration, color)`
|
||||||
|
(шторка). Свои — наследованием от `Transition` (рисование через `TransitionRenderer.Fill`
|
||||||
|
в нормализованных координатах экрана).
|
||||||
|
- Переходы идут по **unscaled**-времени: работают при паузе геймплея (`TimeScale = 0`).
|
||||||
|
- Повторный `Switch` во время перехода заменяет целевую сцену, не перезапуская переход.
|
||||||
|
|
||||||
## Интеграция ECS
|
## Интеграция ECS
|
||||||
|
|
||||||
- На каждую сцену создаётся свой `EntityStore` (мир) и **два** `SystemRoot`:
|
- На каждую сцену создаётся свой `EntityStore` (мир) и **два** `SystemRoot`:
|
||||||
|
|||||||
@@ -82,9 +82,9 @@ public sealed class BounceSystem(RectF bounds) : QuerySystem<Transform2D, Veloci
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Пауза (P), музыка (M), переключение сцены (Tab).</summary>
|
/// <summary>Пауза (P), музыка (M), переключение сцены с переходом (Tab).</summary>
|
||||||
public sealed class SceneHotkeysSystem(
|
public sealed class SceneHotkeysSystem(
|
||||||
EngineContext context, ActionMap<SampleAction> actions, Func<Scene> nextScene) : BaseSystem
|
EngineContext context, ActionMap<SampleAction> actions, Func<Scene> nextScene, Transition transition) : BaseSystem
|
||||||
{
|
{
|
||||||
protected override void OnUpdateGroup()
|
protected override void OnUpdateGroup()
|
||||||
{
|
{
|
||||||
@@ -106,9 +106,9 @@ public sealed class SceneHotkeysSystem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (actions.IsPressed(SampleAction.SwitchScene))
|
if (actions.IsPressed(SampleAction.SwitchScene) && !context.Scenes.IsTransitioning)
|
||||||
{
|
{
|
||||||
context.Scenes.Switch(nextScene());
|
context.Scenes.Switch(nextScene(), transition);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ public sealed class MainScene : Scene
|
|||||||
UpdateSystems.Add(new PlayerControlSystem(player, actions, audio, beep));
|
UpdateSystems.Add(new PlayerControlSystem(player, actions, audio, beep));
|
||||||
UpdateSystems.Add(new BounceSystem(WorldBounds));
|
UpdateSystems.Add(new BounceSystem(WorldBounds));
|
||||||
UpdateSystems.Add(new CameraControlSystem(camera, player, input));
|
UpdateSystems.Add(new CameraControlSystem(camera, player, input));
|
||||||
UpdateSystems.Add(new SceneHotkeysSystem(Context, actions, () => new StressScene()));
|
UpdateSystems.Add(new SceneHotkeysSystem(Context, actions, () => new StressScene(), Transition.Fade(0.8f)));
|
||||||
UpdateSystems.Add(new TitleStatsSystem(Context, renderer, "Main"));
|
UpdateSystems.Add(new TitleStatsSystem(Context, renderer, "Main"));
|
||||||
|
|
||||||
var music = audio.Music;
|
var music = audio.Music;
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ public sealed class StressScene : Scene
|
|||||||
|
|
||||||
UpdateSystems.Add(new BounceSystem(WorldBounds));
|
UpdateSystems.Add(new BounceSystem(WorldBounds));
|
||||||
UpdateSystems.Add(new StressCameraSystem(camera, input));
|
UpdateSystems.Add(new StressCameraSystem(camera, input));
|
||||||
UpdateSystems.Add(new SceneHotkeysSystem(Context, actions, () => new MainScene()));
|
UpdateSystems.Add(new SceneHotkeysSystem(Context, actions, () => new MainScene(), Transition.Wipe(0.8f, Color.DarkSlateBlue)));
|
||||||
UpdateSystems.Add(new TitleStatsSystem(Context, renderer, $"Stress {SpriteCount:N0}"));
|
UpdateSystems.Add(new TitleStatsSystem(Context, renderer, $"Stress {SpriteCount:N0}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,39 +1,103 @@
|
|||||||
namespace MrGameEng.Core;
|
namespace MrGameEng.Core;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Owns the active <see cref="Scene"/>. Scene switches are deferred to the start of the
|
/// Owns the active <see cref="Scene"/>. Plain switches are deferred to the start of the next
|
||||||
/// next update so a scene is never unloaded in the middle of its own frame.
|
/// update so a scene is never unloaded in the middle of its own frame. Switches with a
|
||||||
|
/// <see cref="Transition"/> first cover the old scene, swap at full coverage (hiding even a
|
||||||
|
/// slow <c>OnLoad</c>), then reveal the new one. Transition time is unscaled, so it works
|
||||||
|
/// while gameplay is paused.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class SceneManager
|
public sealed class SceneManager
|
||||||
{
|
{
|
||||||
|
private enum State
|
||||||
|
{
|
||||||
|
Idle,
|
||||||
|
CoveringOut,
|
||||||
|
RevealingIn,
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>The active scene, or null before the first switch is applied.</summary>
|
/// <summary>The active scene, or null before the first switch is applied.</summary>
|
||||||
public Scene? Current { get; private set; }
|
public Scene? Current { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>True while a transition is covering or revealing.</summary>
|
||||||
|
public bool IsTransitioning => _state != State.Idle;
|
||||||
|
|
||||||
private readonly EngineContext _context;
|
private readonly EngineContext _context;
|
||||||
private Scene? _pending;
|
private Scene? _pending;
|
||||||
private bool _hasPending;
|
private bool _hasPending;
|
||||||
|
private Transition? _transition;
|
||||||
|
private State _state;
|
||||||
|
private float _coverage;
|
||||||
|
private TransitionRenderer? _renderer;
|
||||||
|
|
||||||
internal SceneManager(EngineContext context) => _context = context;
|
internal SceneManager(EngineContext context) => _context = context;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Requests a switch to <paramref name="scene"/>. The current scene is unloaded and the new
|
/// Requests a switch to <paramref name="scene"/>. Without a transition the swap happens at
|
||||||
/// one loaded at the start of the next update tick. Passing null unloads the current scene.
|
/// the start of the next update tick; with one, the old scene is first covered.
|
||||||
|
/// Passing null unloads the current scene. Calling during an active transition replaces
|
||||||
|
/// the pending target scene.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Switch(Scene? scene)
|
public void Switch(Scene? scene, Transition? transition = null)
|
||||||
{
|
{
|
||||||
_pending = scene;
|
_pending = scene;
|
||||||
_hasPending = true;
|
_hasPending = true;
|
||||||
|
|
||||||
|
if (_state == State.Idle && transition is not null)
|
||||||
|
{
|
||||||
|
_transition = transition;
|
||||||
|
_state = State.CoveringOut;
|
||||||
|
_coverage = 0f;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Applies a pending switch, then updates the active scene. Called by the host.</summary>
|
/// <summary>Advances a transition and updates the active scene. Called by the host.</summary>
|
||||||
public void Update(GameClock clock)
|
public void Update(GameClock clock)
|
||||||
{
|
{
|
||||||
ApplyPending();
|
switch (_state)
|
||||||
|
{
|
||||||
|
case State.Idle:
|
||||||
|
ApplyPending();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case State.CoveringOut:
|
||||||
|
_coverage = Advance(_coverage, +1f, _transition!.OutDuration, clock.UnscaledDeltaTime);
|
||||||
|
if (_coverage >= 1f)
|
||||||
|
{
|
||||||
|
ApplyPending();
|
||||||
|
_state = State.RevealingIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case State.RevealingIn:
|
||||||
|
_coverage = Advance(_coverage, -1f, _transition!.InDuration, clock.UnscaledDeltaTime);
|
||||||
|
if (_coverage <= 0f)
|
||||||
|
{
|
||||||
|
_state = State.Idle;
|
||||||
|
_transition = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
Current?.Update(clock);
|
Current?.Update(clock);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Draws the active scene. Called by the host.</summary>
|
/// <summary>Draws the active scene and the transition overlay on top. Called by the host.</summary>
|
||||||
public void Draw(GameClock clock) => Current?.Draw(clock);
|
public void Draw(GameClock clock)
|
||||||
|
{
|
||||||
|
Current?.Draw(clock);
|
||||||
|
|
||||||
|
if (_state == State.Idle || !_context.HasGraphicsDevice)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderer ??= new TransitionRenderer(_context.GraphicsDevice);
|
||||||
|
var phase = _state == State.CoveringOut ? TransitionPhase.Out : TransitionPhase.In;
|
||||||
|
_transition!.Draw(_renderer, Math.Clamp(_coverage, 0f, 1f), phase);
|
||||||
|
}
|
||||||
|
|
||||||
internal void ApplyPending()
|
internal void ApplyPending()
|
||||||
{
|
{
|
||||||
@@ -48,4 +112,9 @@ public sealed class SceneManager
|
|||||||
_pending = null;
|
_pending = null;
|
||||||
Current?.Load(_context);
|
Current?.Load(_context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static float Advance(float coverage, float direction, float duration, float deltaTime) =>
|
||||||
|
duration <= 0f
|
||||||
|
? coverage + direction
|
||||||
|
: Math.Clamp(coverage + direction * deltaTime / duration, 0f, 1f);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
|
||||||
|
namespace MrGameEng.Core;
|
||||||
|
|
||||||
|
/// <summary>Phase of a scene transition.</summary>
|
||||||
|
public enum TransitionPhase
|
||||||
|
{
|
||||||
|
/// <summary>The old scene is being covered (coverage grows 0 → 1).</summary>
|
||||||
|
Out,
|
||||||
|
|
||||||
|
/// <summary>The new scene is being revealed (coverage shrinks 1 → 0).</summary>
|
||||||
|
In,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Visual transition between scenes. The scene switch itself happens at full coverage,
|
||||||
|
/// so a slow <c>OnLoad</c> of the next scene is hidden behind the overlay.
|
||||||
|
/// Transitions are stateless and reusable; progress is tracked by <see cref="SceneManager"/>.
|
||||||
|
/// Runs on unscaled time, so it works while gameplay is paused.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class Transition
|
||||||
|
{
|
||||||
|
/// <summary>Seconds the covering phase takes.</summary>
|
||||||
|
public float OutDuration { get; }
|
||||||
|
|
||||||
|
/// <summary>Seconds the revealing phase takes.</summary>
|
||||||
|
public float InDuration { get; }
|
||||||
|
|
||||||
|
/// <summary>Creates a transition with explicit phase durations.</summary>
|
||||||
|
protected Transition(float outDuration, float inDuration)
|
||||||
|
{
|
||||||
|
OutDuration = Math.Max(0f, outDuration);
|
||||||
|
InDuration = Math.Max(0f, inDuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Draws the overlay. <paramref name="coverage"/> is 0 (scene fully visible) to
|
||||||
|
/// 1 (scene fully covered); <paramref name="phase"/> tells which side of the switch this is.
|
||||||
|
/// </summary>
|
||||||
|
public abstract void Draw(TransitionRenderer renderer, float coverage, TransitionPhase phase);
|
||||||
|
|
||||||
|
/// <summary>Fade through a solid color (black by default). Total duration is split between out and in.</summary>
|
||||||
|
public static Transition Fade(float duration = 0.6f, Color? color = null) =>
|
||||||
|
new FadeTransition(duration / 2f, duration / 2f, color ?? Color.Black);
|
||||||
|
|
||||||
|
/// <summary>A curtain wiping across the screen (black by default). Total duration is split between out and in.</summary>
|
||||||
|
public static Transition Wipe(float duration = 0.6f, Color? color = null) =>
|
||||||
|
new WipeTransition(duration / 2f, duration / 2f, color ?? Color.Black);
|
||||||
|
|
||||||
|
private sealed class FadeTransition(float outDuration, float inDuration, Color color)
|
||||||
|
: Transition(outDuration, inDuration)
|
||||||
|
{
|
||||||
|
public override void Draw(TransitionRenderer renderer, float coverage, TransitionPhase phase) =>
|
||||||
|
renderer.Fill(0f, 0f, 1f, 1f, color, coverage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class WipeTransition(float outDuration, float inDuration, Color color)
|
||||||
|
: Transition(outDuration, inDuration)
|
||||||
|
{
|
||||||
|
public override void Draw(TransitionRenderer renderer, float coverage, TransitionPhase phase)
|
||||||
|
{
|
||||||
|
// Out: шторка растёт слева направо; In: уезжает дальше вправо.
|
||||||
|
if (phase == TransitionPhase.Out)
|
||||||
|
{
|
||||||
|
renderer.Fill(0f, 0f, coverage, 1f, color);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
renderer.Fill(1f - coverage, 0f, coverage, 1f, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
|
|
||||||
|
namespace MrGameEng.Core;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Minimal overlay renderer handed to <see cref="Transition.Draw"/>: fills rectangles in
|
||||||
|
/// normalized screen coordinates (0..1 on both axes) over the rendered scene.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class TransitionRenderer
|
||||||
|
{
|
||||||
|
private readonly GraphicsDevice _device;
|
||||||
|
private readonly BasicEffect _effect;
|
||||||
|
private readonly VertexPositionColor[] _vertices = new VertexPositionColor[6];
|
||||||
|
|
||||||
|
internal TransitionRenderer(GraphicsDevice device)
|
||||||
|
{
|
||||||
|
_device = device;
|
||||||
|
_effect = new BasicEffect(device)
|
||||||
|
{
|
||||||
|
VertexColorEnabled = true,
|
||||||
|
TextureEnabled = false,
|
||||||
|
World = Matrix.Identity,
|
||||||
|
View = Matrix.Identity,
|
||||||
|
Projection = Matrix.CreateOrthographicOffCenter(0f, 1f, 1f, 0f, 0f, 1f),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fills a rectangle given in normalized screen coordinates with <paramref name="color"/>
|
||||||
|
/// at the given <paramref name="opacity"/> (0 = invisible, 1 = solid).
|
||||||
|
/// </summary>
|
||||||
|
public void Fill(float x, float y, float width, float height, Color color, float opacity = 1f)
|
||||||
|
{
|
||||||
|
var alpha = (byte)(Math.Clamp(opacity, 0f, 1f) * color.A);
|
||||||
|
var premultiplied = Color.FromNonPremultiplied(color.R, color.G, color.B, alpha);
|
||||||
|
|
||||||
|
var topLeft = new Vector3(x, y, 0f);
|
||||||
|
var topRight = new Vector3(x + width, y, 0f);
|
||||||
|
var bottomLeft = new Vector3(x, y + height, 0f);
|
||||||
|
var bottomRight = new Vector3(x + width, y + height, 0f);
|
||||||
|
|
||||||
|
_vertices[0] = new VertexPositionColor(topLeft, premultiplied);
|
||||||
|
_vertices[1] = new VertexPositionColor(topRight, premultiplied);
|
||||||
|
_vertices[2] = new VertexPositionColor(bottomLeft, premultiplied);
|
||||||
|
_vertices[3] = new VertexPositionColor(bottomLeft, premultiplied);
|
||||||
|
_vertices[4] = new VertexPositionColor(topRight, premultiplied);
|
||||||
|
_vertices[5] = new VertexPositionColor(bottomRight, premultiplied);
|
||||||
|
|
||||||
|
_device.BlendState = BlendState.AlphaBlend;
|
||||||
|
_device.DepthStencilState = DepthStencilState.None;
|
||||||
|
_device.RasterizerState = RasterizerState.CullNone;
|
||||||
|
|
||||||
|
foreach (var pass in _effect.CurrentTechnique.Passes)
|
||||||
|
{
|
||||||
|
pass.Apply();
|
||||||
|
_device.DrawUserPrimitives(PrimitiveType.TriangleList, _vertices, 0, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
using MrGameEng.Core;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace MrGameEng.Core.Tests;
|
||||||
|
|
||||||
|
public class SceneTransitionTests
|
||||||
|
{
|
||||||
|
private sealed class TrackingScene : Scene
|
||||||
|
{
|
||||||
|
public int LoadCount;
|
||||||
|
public int UnloadCount;
|
||||||
|
|
||||||
|
protected override void OnLoad() => LoadCount++;
|
||||||
|
|
||||||
|
protected override void OnUnload() => UnloadCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Tick(EngineContext context, float seconds)
|
||||||
|
{
|
||||||
|
context.Clock.Advance(seconds);
|
||||||
|
context.Scenes.Update(context.Clock);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TransitionSwitch_KeepsOldScene_UntilFullyCovered()
|
||||||
|
{
|
||||||
|
var context = new EngineContext();
|
||||||
|
var first = new TrackingScene();
|
||||||
|
var second = new TrackingScene();
|
||||||
|
context.Scenes.Switch(first);
|
||||||
|
Tick(context, 0.016f);
|
||||||
|
|
||||||
|
// Fade(1.0) → фаза закрытия 0.5 c, фаза открытия 0.5 c.
|
||||||
|
context.Scenes.Switch(second, Transition.Fade(1f));
|
||||||
|
Tick(context, 0.2f);
|
||||||
|
|
||||||
|
Assert.True(context.Scenes.IsTransitioning);
|
||||||
|
Assert.Same(first, context.Scenes.Current);
|
||||||
|
Assert.Equal(0, second.LoadCount);
|
||||||
|
|
||||||
|
Tick(context, 0.4f); // суммарно 0.6 > 0.5 — экран закрыт, своп произошёл
|
||||||
|
|
||||||
|
Assert.Same(second, context.Scenes.Current);
|
||||||
|
Assert.Equal(1, first.UnloadCount);
|
||||||
|
Assert.Equal(1, second.LoadCount);
|
||||||
|
Assert.True(context.Scenes.IsTransitioning); // идёт фаза открытия
|
||||||
|
|
||||||
|
Tick(context, 0.6f); // открытие завершено
|
||||||
|
|
||||||
|
Assert.False(context.Scenes.IsTransitioning);
|
||||||
|
Assert.Same(second, context.Scenes.Current);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Transition_UsesUnscaledTime_WorksWhilePaused()
|
||||||
|
{
|
||||||
|
var context = new EngineContext();
|
||||||
|
var first = new TrackingScene();
|
||||||
|
var second = new TrackingScene();
|
||||||
|
context.Scenes.Switch(first);
|
||||||
|
Tick(context, 0.016f);
|
||||||
|
context.Clock.TimeScale = 0f; // игра на паузе
|
||||||
|
|
||||||
|
context.Scenes.Switch(second, Transition.Fade(0.2f));
|
||||||
|
Tick(context, 0.15f);
|
||||||
|
Tick(context, 0.15f);
|
||||||
|
|
||||||
|
Assert.Same(second, context.Scenes.Current);
|
||||||
|
Assert.False(context.Scenes.IsTransitioning);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SwitchDuringTransition_ReplacesPendingTarget()
|
||||||
|
{
|
||||||
|
var context = new EngineContext();
|
||||||
|
var first = new TrackingScene();
|
||||||
|
var second = new TrackingScene();
|
||||||
|
var third = new TrackingScene();
|
||||||
|
context.Scenes.Switch(first);
|
||||||
|
Tick(context, 0.016f);
|
||||||
|
|
||||||
|
context.Scenes.Switch(second, Transition.Fade(1f));
|
||||||
|
Tick(context, 0.1f);
|
||||||
|
context.Scenes.Switch(third); // передумали, пока экран закрывается
|
||||||
|
|
||||||
|
Tick(context, 0.5f);
|
||||||
|
|
||||||
|
Assert.Same(third, context.Scenes.Current);
|
||||||
|
Assert.Equal(0, second.LoadCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ZeroDurationTransition_SwapsOnNextUpdates()
|
||||||
|
{
|
||||||
|
var context = new EngineContext();
|
||||||
|
var first = new TrackingScene();
|
||||||
|
var second = new TrackingScene();
|
||||||
|
context.Scenes.Switch(first);
|
||||||
|
Tick(context, 0.016f);
|
||||||
|
|
||||||
|
context.Scenes.Switch(second, Transition.Fade(0f));
|
||||||
|
Tick(context, 0.016f);
|
||||||
|
Tick(context, 0.016f);
|
||||||
|
|
||||||
|
Assert.Same(second, context.Scenes.Current);
|
||||||
|
Assert.False(context.Scenes.IsTransitioning);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user