last hope

This commit is contained in:
2025-01-09 06:37:12 +03:00
parent 353117f97a
commit d7678f19df
52 changed files with 1406 additions and 143 deletions

View File

@@ -12,6 +12,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PresenterNative", "Presente
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DoomDeathmatch", "DoomDeathmatch\DoomDeathmatch.csproj", "{4FA5E1F8-B647-4764-9147-B6F5A4E9D1D0}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DoomDeathmatch", "DoomDeathmatch\DoomDeathmatch.csproj", "{4FA5E1F8-B647-4764-9147-B6F5A4E9D1D0}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestUtil", "TestUtil\TestUtil.csproj", "{923DEAFC-55C6-426A-8EE8-D74142FDC342}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameTests", "GameTests\GameTests.csproj", "{C2CE10BB-DB8C-4283-BC22-88CDA8B54DEB}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -42,5 +46,13 @@ Global
{4FA5E1F8-B647-4764-9147-B6F5A4E9D1D0}.Debug|Any CPU.Build.0 = Debug|Any CPU {4FA5E1F8-B647-4764-9147-B6F5A4E9D1D0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4FA5E1F8-B647-4764-9147-B6F5A4E9D1D0}.Release|Any CPU.ActiveCfg = Release|Any CPU {4FA5E1F8-B647-4764-9147-B6F5A4E9D1D0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4FA5E1F8-B647-4764-9147-B6F5A4E9D1D0}.Release|Any CPU.Build.0 = Release|Any CPU {4FA5E1F8-B647-4764-9147-B6F5A4E9D1D0}.Release|Any CPU.Build.0 = Release|Any CPU
{923DEAFC-55C6-426A-8EE8-D74142FDC342}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{923DEAFC-55C6-426A-8EE8-D74142FDC342}.Debug|Any CPU.Build.0 = Debug|Any CPU
{923DEAFC-55C6-426A-8EE8-D74142FDC342}.Release|Any CPU.ActiveCfg = Release|Any CPU
{923DEAFC-55C6-426A-8EE8-D74142FDC342}.Release|Any CPU.Build.0 = Release|Any CPU
{C2CE10BB-DB8C-4283-BC22-88CDA8B54DEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C2CE10BB-DB8C-4283-BC22-88CDA8B54DEB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C2CE10BB-DB8C-4283-BC22-88CDA8B54DEB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C2CE10BB-DB8C-4283-BC22-88CDA8B54DEB}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal

View File

@@ -6,6 +6,14 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DocumentationFile></DocumentationFile>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DocumentationFile></DocumentationFile>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Engine\Engine.csproj"/> <ProjectReference Include="..\Engine\Engine.csproj"/>
</ItemGroup> </ItemGroup>

View File

@@ -18,6 +18,11 @@ public class HealthController : Engine.Scene.Component.Component
/// </summary> /// </summary>
public bool IsAlive => _healthModel.Health > 0; public bool IsAlive => _healthModel.Health > 0;
/// <summary>
/// The current health value of the entity.
/// </summary>
public float Health => _healthModel.Health;
/// <summary> /// <summary>
/// The health model containing the current and maximum health. /// The health model containing the current and maximum health.
/// </summary> /// </summary>

View File

@@ -24,7 +24,7 @@ public class HealthView : Engine.Scene.Component.Component, IView<HealthModel>
} }
/// <inheritdoc/> /// <inheritdoc/>
public virtual void UpdateView(HealthModel parHealthModel) public void UpdateView(HealthModel parHealthModel)
{ {
var percentage = parHealthModel.Health / parHealthModel.MaxHealth * 100; var percentage = parHealthModel.Health / parHealthModel.MaxHealth * 100;
if (parHealthModel.Health != 0) if (parHealthModel.Health != 0)

View File

@@ -26,7 +26,7 @@ public class MovementComponent : Engine.Scene.Component.Component
/// <summary> /// <summary>
/// The drag component for the game object. /// The drag component for the game object.
/// </summary> /// </summary>
private DragComponent _dragComponent = null!; private DragComponent? _dragComponent;
public override void Awake() public override void Awake()
{ {
@@ -34,7 +34,6 @@ public class MovementComponent : Engine.Scene.Component.Component
_dragComponent = GameObject.GetComponent<DragComponent>()!; _dragComponent = GameObject.GetComponent<DragComponent>()!;
ArgumentNullException.ThrowIfNull(_rigidbody); ArgumentNullException.ThrowIfNull(_rigidbody);
ArgumentNullException.ThrowIfNull(_dragComponent);
} }
/// <summary> /// <summary>
@@ -43,7 +42,7 @@ public class MovementComponent : Engine.Scene.Component.Component
/// <param name="parDirection">The direction of movement.</param> /// <param name="parDirection">The direction of movement.</param>
public void ApplyMovement(Vector3 parDirection) public void ApplyMovement(Vector3 parDirection)
{ {
_rigidbody.Force += _dragComponent.Drag * Speed * parDirection.Normalized(); _rigidbody.Force += (_dragComponent?.Drag ?? 1) * Speed * parDirection.Normalized();
} }
/// <summary> /// <summary>
@@ -53,6 +52,6 @@ public class MovementComponent : Engine.Scene.Component.Component
public void ApplyRotation(Vector3 parAxis) public void ApplyRotation(Vector3 parAxis)
{ {
var radiansPerSecond = MathHelper.DegreesToRadians(RotationSpeed); var radiansPerSecond = MathHelper.DegreesToRadians(RotationSpeed);
_rigidbody.Torque += _dragComponent.RotationalDrag * radiansPerSecond * parAxis.Normalized(); _rigidbody.Torque += (_dragComponent?.RotationalDrag ?? 1) * radiansPerSecond * parAxis.Normalized();
} }
} }

View File

@@ -30,7 +30,7 @@ public class WeaponController : Engine.Scene.Component.Component
/// <summary> /// <summary>
/// View responsible for displaying weapon information and animations. /// View responsible for displaying weapon information and animations.
/// </summary> /// </summary>
private IWeaponView _weaponView = null!; private IWeaponView? _weaponView;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="WeaponController"/> class with an initial weapon. /// Initializes a new instance of the <see cref="WeaponController"/> class with an initial weapon.
@@ -43,7 +43,7 @@ public class WeaponController : Engine.Scene.Component.Component
public override void Awake() public override void Awake()
{ {
_weaponView = GameObject.GetComponent<IWeaponView>()!; _weaponView = GameObject.GetComponent<IWeaponView>();
_weaponModel.OnWeaponSelected += WeaponSelected; _weaponModel.OnWeaponSelected += WeaponSelected;
WeaponSelected(null, _weaponModel.SelectedWeapon); WeaponSelected(null, _weaponModel.SelectedWeapon);
@@ -63,7 +63,7 @@ public class WeaponController : Engine.Scene.Component.Component
_weaponModel.SelectedWeapon.Ammo--; _weaponModel.SelectedWeapon.Ammo--;
OnWeaponShot?.Invoke(_weaponModel.SelectedWeapon); OnWeaponShot?.Invoke(_weaponModel.SelectedWeapon);
_weaponView.PlayFireAnimation(); _weaponView?.PlayFireAnimation();
return true; return true;
} }
@@ -178,7 +178,7 @@ public class WeaponController : Engine.Scene.Component.Component
} }
parNewWeapon.OnAmmoChanged += AmmoChanged; parNewWeapon.OnAmmoChanged += AmmoChanged;
_weaponView.UpdateView(parNewWeapon); _weaponView?.UpdateView(parNewWeapon);
AmmoChanged(parNewWeapon); AmmoChanged(parNewWeapon);
} }
@@ -190,6 +190,6 @@ public class WeaponController : Engine.Scene.Component.Component
{ {
var ammoData = new AmmoData { Ammo = parWeapon.Ammo, MaxAmmo = parWeapon.MaxAmmo }; var ammoData = new AmmoData { Ammo = parWeapon.Ammo, MaxAmmo = parWeapon.MaxAmmo };
_weaponView.UpdateView(ammoData); _weaponView?.UpdateView(ammoData);
} }
} }

View File

@@ -154,16 +154,9 @@ public class RigidbodyComponent : Engine.Scene.Component.Component
_angularAcceleration = Torque / MomentOfInertia; _angularAcceleration = Torque / MomentOfInertia;
AngularVelocity += _angularAcceleration * (float)parDeltaTime; AngularVelocity += _angularAcceleration * (float)parDeltaTime;
// Update rotation using quaternion math GameObject.Transform.Rotation *= Quaternion.FromAxisAngle(Vector3.UnitX, AngularVelocity.X * (float)parDeltaTime);
var rotation = GameObject.Transform.Rotation; GameObject.Transform.Rotation *= Quaternion.FromAxisAngle(Vector3.UnitY, AngularVelocity.Y * (float)parDeltaTime);
var angularVelocityQuat = new Quaternion(AngularVelocity, 0.0f); GameObject.Transform.Rotation *= Quaternion.FromAxisAngle(Vector3.UnitZ, AngularVelocity.Z * (float)parDeltaTime);
// Quaternion rotation integration: Δq = 0.5 * angularVelocityQuat * rotation
var deltaRotation = 0.5f * angularVelocityQuat * rotation;
rotation += deltaRotation * (float)parDeltaTime;
rotation.Normalize(); // Ensure the quaternion remains normalized
GameObject.Transform.Rotation = rotation;
Torque = Vector3.Zero; Torque = Vector3.Zero;
} }

View File

@@ -58,9 +58,9 @@ public class TextAlignComponent : Engine.Scene.Component.Component
{ {
return Alignment switch return Alignment switch
{ {
Align.Left => new Vector2(0, -parSize.Y / 2), Align.Left => new Vector2(0, parSize.Y / 2 - _textRenderer.Font.Metadata.Metrics.Ascender),
Align.Center => new Vector2(-parSize.X / 2, -parSize.Y / 2), Align.Center => new Vector2(-parSize.X / 2, parSize.Y / 2 - _textRenderer.Font.Metadata.Metrics.Ascender),
Align.Right => new Vector2(-parSize.X, -parSize.Y / 2), Align.Right => new Vector2(-parSize.X, parSize.Y / 2 - _textRenderer.Font.Metadata.Metrics.Ascender),
_ => throw new ArgumentOutOfRangeException(nameof(Alignment), Alignment, null) _ => throw new ArgumentOutOfRangeException(nameof(Alignment), Alignment, null)
}; };
} }

View File

@@ -180,9 +180,14 @@ public static class MainScene
var (rulesObject, rulesUi, _) = UiPrefabs.CreateTextUi(parScene, parUiContainer, var (rulesObject, rulesUi, _) = UiPrefabs.CreateTextUi(parScene, parUiContainer,
UiPrefabs.GetDoomFont(), "Правила"); UiPrefabs.GetDoomFont(), "Правила");
var (actualRulesObject, actualRulesUi, _) = UiPrefabs.CreateTextUi(parScene, parUiContainer,
UiPrefabs.GetDoomFont(), "Игрок управляет персонажем, который может передвигаться, собирать предметы и использовать оружие.\nВ игре два вида оружия: пистолет и дробовик, каждое с уникальными характеристиками и ограничением по боеприпасам.\nПротивники делятся на демонов, которые преследуют игрока и наносят ближний урон, и импом,\nкоторые атакуют издалека и создают огненные шары.\nНа уровне случайно появляются предметы, такие как оружие и аптечки, которые восполняют здоровье или боеприпасы.\nЗа уничтожение врагов игрок получает очки, отображаемые в счетчике.\nЛучшие результаты сохраняются в таблице рекордов.");
actualRulesObject.Transform.Scale.Xy = new Vector2(0.5f);
var (stackObject, stack) = UiPrefabs.CreateStackUi(parScene, var (stackObject, stack) = UiPrefabs.CreateStackUi(parScene,
new StackComponent { Offset = new Vector2(0, -1f), Container = parUiContainer, Children = { rulesUi, backUi } }); new StackComponent { Offset = new Vector2(0, -1.5f), Container = parUiContainer, Children = { rulesUi, actualRulesUi, backUi } });
stackObject.Transform.Size.Xy = new Vector2(1f, 6f); stackObject.Transform.Size.Xy = new Vector2(1f, 9f);
var (selectorObject, selector) = UiPrefabs.CreateSelectorUi(parScene, new SelectorComponent { Children = { backUi } }); var (selectorObject, selector) = UiPrefabs.CreateSelectorUi(parScene, new SelectorComponent { Children = { backUi } });
@@ -191,6 +196,7 @@ public static class MainScene
parScene.AddChild(parentObject, stackObject); parScene.AddChild(parentObject, stackObject);
parScene.AddChild(stackObject, rulesObject); parScene.AddChild(stackObject, rulesObject);
parScene.AddChild(stackObject, actualRulesObject);
parScene.AddChild(stackObject, backUiObject); parScene.AddChild(stackObject, backUiObject);
return parentObject; return parentObject;

View File

@@ -19,7 +19,7 @@ public static class PlayerPrefab
perspectiveCameraObject.Transform.Translation.Z = 2; perspectiveCameraObject.Transform.Translation.Z = 2;
var playerObject = GameObjectUtil.CreateGameObject(parScene, [ var playerObject = GameObjectUtil.CreateGameObject(parScene, [
new RigidbodyComponent(), new RigidbodyComponent(),
new DragComponent { Drag = 10f, RotationalDrag = 10f, }, new DragComponent { Drag = 10f, RotationalDrag = 20f, },
new AABBColliderComponent new AABBColliderComponent
{ {

View File

@@ -42,10 +42,10 @@ public static class UiPrefabs
public static (GameObject, UiContainerComponent, (GameObject, TextRenderer)) CreateTextUi(Engine.Scene.Scene parScene, public static (GameObject, UiContainerComponent, (GameObject, TextRenderer)) CreateTextUi(Engine.Scene.Scene parScene,
UiContainerComponent parContainer, UiContainerComponent parContainer,
Font parFont, string parText, Align parAlign = Align.Center, Font? parFont, string parText, Align parAlign = Align.Center,
RenderLayer? parRenderLayer = null, float parScale = 1) RenderLayer? parRenderLayer = null, float parScale = 1)
{ {
var size = parFont.Measure(parText); var size = parFont?.Measure(parText) ?? Vector2.Zero;
var outerObject = new GameObject var outerObject = new GameObject
{ {
Transform = Transform =

View File

@@ -15,6 +15,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<InternalsVisibleTo Include="TestUtil"/>
<InternalsVisibleTo Include="EngineTests"/> <InternalsVisibleTo Include="EngineTests"/>
<InternalsVisibleTo Include="PresenterConsole"/> <InternalsVisibleTo Include="PresenterConsole"/>
<InternalsVisibleTo Include="PresenterWpf"/> <InternalsVisibleTo Include="PresenterWpf"/>

View File

@@ -0,0 +1,41 @@
using Engine.Graphics;
using Engine.Input;
using Engine.Resource;
using Engine.Scene;
namespace Engine.Context;
/// <summary>
/// A context for the engine, providing access to the engine's services.
/// </summary>
public class EngineContext : IContext
{
public IInputHandler InputHandler => _engine.InputHandler!;
public IResourceManager AssetResourceManager => _engine.AssetResourceManager;
public ISceneManager SceneManager => _engine.SceneManager;
public IRenderer Renderer => _engine.Renderer;
public string DataFolder => _engine.DataFolder;
/// <summary>
/// The engine instance associated with this context.
/// </summary>
private readonly Engine _engine;
/// <summary>
/// Initializes a new instance of the <see cref="EngineContext"/> class.
/// </summary>
/// <param name="parEngine">The engine instance to use for this context.</param>
public EngineContext(Engine parEngine)
{
_engine = parEngine;
}
public void Close()
{
_engine.Close();
}
}

View File

@@ -0,0 +1,42 @@
using Engine.Graphics;
using Engine.Input;
using Engine.Resource;
using Engine.Scene;
namespace Engine.Context;
/// <summary>
/// Defines an interface for the engine's context, providing access to the engine's services.
/// </summary>
public interface IContext
{
/// <summary>
/// The input handler for the engine.
/// </summary>
public IInputHandler InputHandler { get; }
/// <summary>
/// The resource manager for the engine.
/// </summary>
public IResourceManager AssetResourceManager { get; }
/// <summary>
/// The scene manager for the engine.
/// </summary>
public ISceneManager SceneManager { get; }
/// <summary>
/// The renderer for the engine.
/// </summary>
public IRenderer Renderer { get; }
/// <summary>
/// The data folder for the engine.
/// </summary>
public string DataFolder { get; }
/// <summary>
/// Closes the engine, shutting down any running systems and freeing resources.
/// </summary>
public void Close();
}

View File

@@ -3,6 +3,7 @@ using System.Text;
using Engine.Asset; using Engine.Asset;
using Engine.Asset.Font; using Engine.Asset.Font;
using Engine.Asset.Mesh; using Engine.Asset.Mesh;
using Engine.Context;
using Engine.Graphics; using Engine.Graphics;
using Engine.Graphics.Pipeline; using Engine.Graphics.Pipeline;
using Engine.Graphics.Pixel; using Engine.Graphics.Pixel;
@@ -12,6 +13,7 @@ using Engine.Input;
using Engine.Resource; using Engine.Resource;
using Engine.Resource.Loader; using Engine.Resource.Loader;
using Engine.Scene; using Engine.Scene;
using Engine.Util;
using OpenTK.Mathematics; using OpenTK.Mathematics;
using OpenTK.Windowing.Common; using OpenTK.Windowing.Common;
using OpenTK.Windowing.Desktop; using OpenTK.Windowing.Desktop;
@@ -33,7 +35,7 @@ public sealed class Engine
/// <summary> /// <summary>
/// The scene manager for managing and updating scenes. /// The scene manager for managing and updating scenes.
/// </summary> /// </summary>
public SceneManager SceneManager { get; } = new(); public ISceneManager SceneManager => _sceneManager;
/// <summary> /// <summary>
/// The resource manager responsible for asset management. /// The resource manager responsible for asset management.
@@ -87,6 +89,11 @@ public sealed class Engine
/// </summary> /// </summary>
internal Window Window { get; } internal Window Window { get; }
/// <summary>
/// The scene manager for managing and updating scenes.
/// </summary>
private readonly SceneManager _sceneManager = new();
/// <summary> /// <summary>
/// The logger instance used by the engine. /// The logger instance used by the engine.
/// </summary> /// </summary>
@@ -154,6 +161,8 @@ public sealed class Engine
Renderer = new Renderer(this, parWidth, parHeight, settings); Renderer = new Renderer(this, parWidth, parHeight, settings);
Window = new Window(this, Renderer.NativeWindow, parHeadless); Window = new Window(this, Renderer.NativeWindow, parHeadless);
EngineUtil.SetContext(new EngineContext(this));
} }
/// <summary> /// <summary>
@@ -250,7 +259,7 @@ public sealed class Engine
} }
} }
SceneManager.Render(); _sceneManager.Render();
} }
Monitor.Exit(_sceneLock); Monitor.Exit(_sceneLock);
@@ -283,7 +292,7 @@ public sealed class Engine
{ {
try try
{ {
SceneManager.Update(deltaTime); _sceneManager.Update(deltaTime);
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@@ -10,22 +10,22 @@ namespace Engine.Graphics;
/// <summary> /// <summary>
/// A generic renderer that supports rendering quads, meshes, and text. /// A generic renderer that supports rendering quads, meshes, and text.
/// </summary> /// </summary>
public class GenericRenderer : IRenderer public class GenericRenderer : IGenericRenderer
{ {
/// <summary> /// <summary>
/// Provides functionality to render quads. /// Provides functionality to render quads.
/// </summary> /// </summary>
public QuadRenderer QuadRenderer => _quadRenderer ??= new QuadRenderer(_engine, 1024 * 8); public IQuadRenderer QuadRenderer => _quadRenderer ??= new QuadRenderer(_engine, 1024 * 8);
/// <summary> /// <summary>
/// Provides functionality to render any type of mesh. /// Provides functionality to render any type of mesh.
/// </summary> /// </summary>
public AnyMeshRenderer AnyMeshRenderer => _anyMeshRenderer ??= new AnyMeshRenderer(_engine, 1024); public IAnyMeshRenderer AnyMeshRenderer => _anyMeshRenderer ??= new AnyMeshRenderer(_engine, 1024);
/// <summary> /// <summary>
/// Provides functionality to render text. /// Provides functionality to render text.
/// </summary> /// </summary>
public TextRenderer TextRenderer => _textRenderer ??= new TextRenderer(_engine, 1024 * 8); public ITextRenderer TextRenderer => _textRenderer ??= new TextRenderer(_engine, 1024 * 8);
/// <summary> /// <summary>
/// The framebuffer used for rendering. /// The framebuffer used for rendering.
@@ -73,13 +73,19 @@ public class GenericRenderer : IRenderer
.Build(); .Build();
} }
/// <inheritdoc/> /// <summary>
/// Prepares the renderer for a new frame.
/// </summary>
public void StartFrame() public void StartFrame()
{ {
_frameStarted = true; _frameStarted = true;
} }
/// <inheritdoc/> /// <summary>
/// Finalizes the rendering pipeline for the current frame.
/// </summary>
/// <param name="parProjectionMatrix">The projection matrix to use for rendering.</param>
/// <param name="parViewMatrix">The view matrix to use for rendering.</param>
public void EndFrame(in Matrix4 parProjectionMatrix, in Matrix4 parViewMatrix) public void EndFrame(in Matrix4 parProjectionMatrix, in Matrix4 parViewMatrix)
{ {
if (!_frameStarted) if (!_frameStarted)
@@ -115,7 +121,11 @@ public class GenericRenderer : IRenderer
_frameStarted = false; _frameStarted = false;
} }
/// <inheritdoc/> /// <summary>
/// Resizes the renderer to accommodate changes in viewport dimensions.
/// </summary>
/// <param name="parWidth">The new width of the viewport.</param>
/// <param name="parHeight">The new height of the viewport.</param>
public void Resize(int parWidth, int parHeight) public void Resize(int parWidth, int parHeight)
{ {
_framebuffer.Resize(parWidth, parHeight); _framebuffer.Resize(parWidth, parHeight);

View File

@@ -0,0 +1,26 @@
using Engine.Graphics.Render.Mesh;
using Engine.Graphics.Render.Quad;
using Engine.Graphics.Render.Text;
namespace Engine.Graphics;
/// <summary>
/// Interface defining the essential functionality for a renderer.
/// </summary>
public interface IGenericRenderer
{
/// <summary>
/// Provides functionality to render quads.
/// </summary>
public IQuadRenderer QuadRenderer { get; }
/// <summary>
/// Provides functionality to render any type of mesh.
/// </summary>
public IAnyMeshRenderer AnyMeshRenderer { get; }
/// <summary>
/// Provides functionality to render text.
/// </summary>
public ITextRenderer TextRenderer { get; }
}

View File

@@ -1,28 +1,14 @@
using OpenTK.Mathematics; using Engine.Graphics.Pipeline;
namespace Engine.Graphics; namespace Engine.Graphics;
/// <summary> public interface IRenderer
/// Interface defining the essential functionality for a renderer.
/// </summary>
internal interface IRenderer
{ {
/// <summary> /// <summary>
/// Prepares the renderer for a new frame. /// Retrieves the renderer for the specified render layer.
/// </summary> /// </summary>
public void StartFrame(); /// <param name="parRenderLayer">The render layer to retrieve.</param>
/// <returns>The <see cref="GenericRenderer"/> for the specified render layer.</returns>
/// <summary> /// <exception cref="InvalidOperationException">Thrown if the render layer does not exist.</exception>
/// Finalizes the rendering pipeline for the current frame. public IGenericRenderer this[RenderLayer parRenderLayer] { get; }
/// </summary>
/// <param name="parProjectionMatrix">The projection matrix to use for rendering.</param>
/// <param name="parViewMatrix">The view matrix to use for rendering.</param>
public void EndFrame(in Matrix4 parProjectionMatrix, in Matrix4 parViewMatrix);
/// <summary>
/// Resizes the renderer to accommodate changes in viewport dimensions.
/// </summary>
/// <param name="parWidth">The new width of the viewport.</param>
/// <param name="parHeight">The new height of the viewport.</param>
public void Resize(int parWidth, int parHeight);
} }

View File

@@ -7,7 +7,7 @@ namespace Engine.Graphics.Render.Mesh;
/// A renderer class that manages multiple meshes and delegates rendering to individual mesh renderers. /// A renderer class that manages multiple meshes and delegates rendering to individual mesh renderers.
/// Handles batching of mesh instances and ensures that only the necessary mesh renderers are created. /// Handles batching of mesh instances and ensures that only the necessary mesh renderers are created.
/// </summary> /// </summary>
public class AnyMeshRenderer public class AnyMeshRenderer : IAnyMeshRenderer
{ {
/// <summary> /// <summary>
/// A dictionary that maps each mesh to its corresponding <see cref="MeshRenderer"/>. /// A dictionary that maps each mesh to its corresponding <see cref="MeshRenderer"/>.
@@ -40,14 +40,8 @@ public class AnyMeshRenderer
_program = parEngine.EngineResourceManager.Load<Program>("shader/mesh"); _program = parEngine.EngineResourceManager.Load<Program>("shader/mesh");
} }
/// <summary> /// <inheritdoc/>
/// Commits an instance of a mesh to the renderer, adding it to the render queue with the specified model matrix and optional texture. public void Commit(Asset.Mesh.Mesh parMesh, in Matrix4 parModelMatrix, Texture.Texture? parAlbedo = null)
/// If the mesh is not already being tracked, a new <see cref="MeshRenderer"/> will be created for it.
/// </summary>
/// <param name="parMesh">The mesh to render.</param>
/// <param name="parModelMatrix">The model transformation matrix to apply to the mesh.</param>
/// <param name="parAlbedo">An optional texture to apply to the mesh. If null, no texture is applied.</param>
public void Commit(Asset.Mesh.Mesh parMesh, Matrix4 parModelMatrix, Texture.Texture? parAlbedo = null)
{ {
if (_meshRenderers.TryGetValue(parMesh, out var meshRenderer)) if (_meshRenderers.TryGetValue(parMesh, out var meshRenderer))
{ {

View File

@@ -0,0 +1,18 @@
using OpenTK.Mathematics;
namespace Engine.Graphics.Render.Mesh;
/// <summary>
/// Defines an interface for a renderer that can render any type of mesh.
/// </summary>
public interface IAnyMeshRenderer
{
/// <summary>
/// Commits an instance of a mesh to the renderer, adding it to the render queue with the specified model matrix and optional texture.
/// If the mesh is not already being tracked, a new <see cref="MeshRenderer"/> will be created for it.
/// </summary>
/// <param name="parMesh">The mesh to render.</param>
/// <param name="parModelMatrix">The model transformation matrix to apply to the mesh.</param>
/// <param name="parAlbedo">An optional texture to apply to the mesh. If null, no texture is applied.</param>
public void Commit(Asset.Mesh.Mesh parMesh, in Matrix4 parModelMatrix, Texture.Texture? parAlbedo = null);
}

View File

@@ -52,7 +52,7 @@ public class MeshRenderer : InstancedRenderer<Asset.Mesh.Mesh.Vertex, MeshInstan
/// </summary> /// </summary>
/// <param name="parModelMatrix">The model transformation matrix for this instance.</param> /// <param name="parModelMatrix">The model transformation matrix for this instance.</param>
/// <param name="parTexture">An optional texture to apply to the mesh. If null, no texture is applied.</param> /// <param name="parTexture">An optional texture to apply to the mesh. If null, no texture is applied.</param>
public void Commit(Matrix4 parModelMatrix, Texture.Texture? parTexture = null) public void Commit(in Matrix4 parModelMatrix, Texture.Texture? parTexture = null)
{ {
if (_queuedInstanceCount >= _instanceCount) if (_queuedInstanceCount >= _instanceCount)
{ {

View File

@@ -0,0 +1,17 @@
using OpenTK.Mathematics;
namespace Engine.Graphics.Render.Quad;
/// <summary>
/// Defines an interface for a renderer that can render quads.
/// </summary>
public interface IQuadRenderer
{
/// <summary>
/// Commits an instance to the renderer, adding it to the queue with the specified model matrix, color, and optional texture.
/// </summary>
/// <param name="parModelMatrix">The model transformation matrix for this instance.</param>
/// <param name="parColor">The color to apply to this instance.</param>
/// <param name="parTexture">An optional texture to apply to the quad. If null, no texture is applied.</param>
public void Commit(in Matrix4 parModelMatrix, in Vector4 parColor, Texture.Texture? parTexture = null);
}

View File

@@ -9,7 +9,7 @@ namespace Engine.Graphics.Render.Quad;
/// A renderer class for rendering quadrilaterals (quads) using instancing. /// A renderer class for rendering quadrilaterals (quads) using instancing.
/// Supports dynamic texture binding and manages the state for rendering multiple instances of quads. /// Supports dynamic texture binding and manages the state for rendering multiple instances of quads.
/// </summary> /// </summary>
public class QuadRenderer : InstancedRenderer<QuadCommonVertex, QuadInstanceVertex> public class QuadRenderer : InstancedRenderer<QuadCommonVertex, QuadInstanceVertex>, IQuadRenderer
{ {
/// <summary> /// <summary>
/// Maps textures to texture units with a limit of 16 texture units. /// Maps textures to texture units with a limit of 16 texture units.
@@ -47,12 +47,7 @@ public class QuadRenderer : InstancedRenderer<QuadCommonVertex, QuadInstanceVert
{ {
} }
/// <summary> /// <inheritdoc/>
/// Commits an instance to the renderer, adding it to the queue with the specified model matrix, color, and optional texture.
/// </summary>
/// <param name="parModelMatrix">The model transformation matrix for this instance.</param>
/// <param name="parColor">The color to apply to this instance.</param>
/// <param name="parTexture">An optional texture to apply to the quad. If null, no texture is applied.</param>
public void Commit(in Matrix4 parModelMatrix, in Vector4 parColor, Texture.Texture? parTexture = null) public void Commit(in Matrix4 parModelMatrix, in Vector4 parColor, Texture.Texture? parTexture = null)
{ {
if (_queuedInstanceCount >= _instanceCount) if (_queuedInstanceCount >= _instanceCount)

View File

@@ -0,0 +1,19 @@
using Engine.Asset.Font;
using OpenTK.Mathematics;
namespace Engine.Graphics.Render.Text;
/// <summary>
/// Defines an interface for a renderer that can render text.
/// </summary>
public interface ITextRenderer
{
/// <summary>
/// Commits a string of text to the renderer, creating the necessary glyphs and adding them to the render queue.
/// </summary>
/// <param name="parFont">The font to use for rendering the text.</param>
/// <param name="parText">The text string to render.</param>
/// <param name="parColor">The color to apply to the text.</param>
/// <param name="parModelMatrix">The model transformation matrix to apply to the text.</param>
public void Commit(Font parFont, string parText, in Vector4 parColor, in Matrix4 parModelMatrix);
}

View File

@@ -11,7 +11,7 @@ namespace Engine.Graphics.Render.Text;
/// A renderer class for rendering text using glyphs from a font atlas. /// A renderer class for rendering text using glyphs from a font atlas.
/// Handles dynamic font rendering with support for textures. /// Handles dynamic font rendering with support for textures.
/// </summary> /// </summary>
public class TextRenderer public class TextRenderer : ITextRenderer
{ {
/// <summary> /// <summary>
/// The shader program used for rendering the text. /// The shader program used for rendering the text.
@@ -80,14 +80,8 @@ public class TextRenderer
_vertexArray.BindVertexBuffer(_glyphVertexBuffer); _vertexArray.BindVertexBuffer(_glyphVertexBuffer);
} }
/// <summary> /// <inheritdoc/>
/// Commits a string of text to the renderer, creating the necessary glyphs and adding them to the render queue. public void Commit(Font parFont, string parText, in Vector4 parColor, in Matrix4 parModelMatrix)
/// </summary>
/// <param name="parFont">The font to use for rendering the text.</param>
/// <param name="parText">The text string to render.</param>
/// <param name="parColor">The color to apply to the text.</param>
/// <param name="parModelMatrix">The model transformation matrix to apply to the text.</param>
public void Commit(Font parFont, string parText, Vector4 parColor, in Matrix4 parModelMatrix)
{ {
if (_queuedCharacterCount >= _characterCount) if (_queuedCharacterCount >= _characterCount)
{ {

View File

@@ -10,7 +10,7 @@ namespace Engine.Graphics;
/// <summary> /// <summary>
/// Handles the rendering pipeline, manages render layers, and provides tools for rendering graphics in the engine. /// Handles the rendering pipeline, manages render layers, and provides tools for rendering graphics in the engine.
/// </summary> /// </summary>
public class Renderer public class Renderer : IRenderer
{ {
/// <summary> /// <summary>
/// The width of the viewport. /// The width of the viewport.
@@ -89,13 +89,8 @@ public class Renderer
} }
} }
/// <summary> /// <inheritdoc/>
/// Retrieves the renderer for the specified render layer. public IGenericRenderer this[RenderLayer parRenderLayer]
/// </summary>
/// <param name="parRenderLayer">The render layer to retrieve.</param>
/// <returns>The <see cref="GenericRenderer"/> for the specified render layer.</returns>
/// <exception cref="InvalidOperationException">Thrown if the render layer does not exist.</exception>
public GenericRenderer this[RenderLayer parRenderLayer]
{ {
get get
{ {

View File

@@ -1,5 +1,6 @@
using Engine.Graphics.Pipeline; using Engine.Graphics.Pipeline;
using Engine.Graphics.Texture; using Engine.Graphics.Texture;
using Engine.Util;
using OpenTK.Mathematics; using OpenTK.Mathematics;
namespace Engine.Scene.Component.BuiltIn.Renderer; namespace Engine.Scene.Component.BuiltIn.Renderer;
@@ -32,7 +33,7 @@ public class Box2DRenderer : Component
/// <inheritdoc/> /// <inheritdoc/>
public override void Render() public override void Render()
{ {
Engine.Instance.Renderer[RenderLayer].QuadRenderer EngineUtil.Renderer[RenderLayer].QuadRenderer
.Commit(GameObject.Transform.FullTransformMatrix, Color, Texture); .Commit(GameObject.Transform.FullTransformMatrix, Color, Texture);
} }
} }

View File

@@ -1,6 +1,7 @@
using Engine.Asset.Mesh; using Engine.Asset.Mesh;
using Engine.Graphics.Pipeline; using Engine.Graphics.Pipeline;
using Engine.Graphics.Texture; using Engine.Graphics.Texture;
using Engine.Util;
namespace Engine.Scene.Component.BuiltIn.Renderer; namespace Engine.Scene.Component.BuiltIn.Renderer;
@@ -27,7 +28,7 @@ public class MeshRenderer : Component
/// <inheritdoc/> /// <inheritdoc/>
public override void Render() public override void Render()
{ {
Engine.Instance.Renderer[RenderLayer].AnyMeshRenderer EngineUtil.Renderer[RenderLayer].AnyMeshRenderer
.Commit(Mesh, GameObject.Transform.FullTransformMatrix, Albedo); .Commit(Mesh, GameObject.Transform.FullTransformMatrix, Albedo);
} }
} }

View File

@@ -1,5 +1,6 @@
using Engine.Asset.Font; using Engine.Asset.Font;
using Engine.Graphics.Pipeline; using Engine.Graphics.Pipeline;
using Engine.Util;
using OpenTK.Mathematics; using OpenTK.Mathematics;
namespace Engine.Scene.Component.BuiltIn.Renderer; namespace Engine.Scene.Component.BuiltIn.Renderer;
@@ -12,7 +13,7 @@ public class TextRenderer : Component
/// <summary> /// <summary>
/// The font used for rendering the text. /// The font used for rendering the text.
/// </summary> /// </summary>
public Font Font { get; set; } = null!; public Font? Font { get; set; }
/// <summary> /// <summary>
/// The color of the text. /// The color of the text.
@@ -37,12 +38,12 @@ public class TextRenderer : Component
/// <inheritdoc/> /// <inheritdoc/>
public override void Render() public override void Render()
{ {
if (Text == null) if (Text == null || Font == null)
{ {
return; return;
} }
Engine.Instance.Renderer[RenderLayer].TextRenderer EngineUtil.Renderer[RenderLayer].TextRenderer
.Commit(Font, Text, Color, GameObject.Transform.FullTransformMatrix); .Commit(Font, Text, Color, GameObject.Transform.FullTransformMatrix);
} }
} }

View File

@@ -0,0 +1,18 @@
namespace Engine.Scene;
/// <summary>
/// Defines an interface for a scene manager that handles the current scene and scene transitions.
/// </summary>
public interface ISceneManager
{
/// <summary>
/// The current scene, or null if no scene is active.
/// </summary>
public Scene? CurrentScene { get; }
/// <summary>
/// Transitions to the specified scene.
/// </summary>
/// <param name="parScene">The generator function for the scene to transition to.</param>
public void TransitionTo(Func<Scene>? parScene);
}

View File

@@ -17,7 +17,7 @@ public class Scene : IUpdate, IRender
/// <summary> /// <summary>
/// The time scale for updating the scene. A value of 1.0 means normal speed. /// The time scale for updating the scene. A value of 1.0 means normal speed.
/// </summary> /// </summary>
public float TimeScale { get; set; } = 1.0f; public double TimeScale { get; set; } = 1.0;
/// <summary> /// <summary>
/// A hierarchy of game objects in the scene. /// A hierarchy of game objects in the scene.
@@ -206,7 +206,7 @@ public class Scene : IUpdate, IRender
/// <summary> /// <summary>
/// Processes changes in the hierarchy and scene actions. /// Processes changes in the hierarchy and scene actions.
/// </summary> /// </summary>
private void ProcessChanges() internal void ProcessChanges()
{ {
Hierarchy.ProcessChanges(); Hierarchy.ProcessChanges();

View File

@@ -3,7 +3,7 @@
/// <summary> /// <summary>
/// Manages the current scene in the game, handles scene transitions, and facilitates updating and rendering the current scene. /// Manages the current scene in the game, handles scene transitions, and facilitates updating and rendering the current scene.
/// </summary> /// </summary>
public class SceneManager : IUpdate, IRender public class SceneManager : ISceneManager, IUpdate, IRender
{ {
/// <summary> /// <summary>
/// The current scene being managed by the scene manager. /// The current scene being managed by the scene manager.

View File

@@ -1,4 +1,6 @@
using Engine.Input; using Engine.Context;
using Engine.Graphics;
using Engine.Input;
using Engine.Resource; using Engine.Resource;
using Engine.Scene; using Engine.Scene;
@@ -13,38 +15,47 @@ public static class EngineUtil
/// <summary> /// <summary>
/// The engine's input handler, which processes user input. /// The engine's input handler, which processes user input.
/// </summary> /// </summary>
public static IInputHandler InputHandler => Engine.Instance.InputHandler!; public static IInputHandler InputHandler => CONTEXT_INSTANCE.InputHandler;
/// <summary> /// <summary>
/// The engine's scene manager, which handles the current scene and scene transitions. /// The engine's scene manager, which handles the current scene and scene transitions.
/// </summary> /// </summary>
public static SceneManager SceneManager => Engine.Instance.SceneManager; public static ISceneManager SceneManager => CONTEXT_INSTANCE.SceneManager;
/// <summary> /// <summary>
/// The engine's asset resource manager, which handles loading and caching of assets. /// The engine's asset resource manager, which handles loading and caching of assets.
/// </summary> /// </summary>
public static IResourceManager AssetResourceManager => Engine.Instance.AssetResourceManager; public static IResourceManager AssetResourceManager => CONTEXT_INSTANCE.AssetResourceManager;
/// <summary>
/// The engine's renderer, which handles rendering operations.
/// </summary>
public static IRenderer Renderer => CONTEXT_INSTANCE.Renderer;
/// <summary> /// <summary>
/// The engine's data folder, which contains assets and other data files. /// The engine's data folder, which contains assets and other data files.
/// </summary> /// </summary>
public static string DataFolder => Engine.Instance.DataFolder; public static string DataFolder => CONTEXT_INSTANCE.DataFolder;
/// <summary>
/// Creates a game object and adds it to the current scene.
/// </summary>
/// <param name="parGameObject">The game object to be added to the scene.</param>
public static void CreateObject(GameObject parGameObject)
{
var scene = Engine.Instance.SceneManager.CurrentScene!;
scene.Add(parGameObject);
}
/// <summary> /// <summary>
/// Closes the engine, shutting down any running systems and freeing resources. /// Closes the engine, shutting down any running systems and freeing resources.
/// </summary> /// </summary>
public static void Close() public static void Close()
{ {
Engine.Instance.Close(); CONTEXT_INSTANCE.Close();
}
/// <summary>
/// The engine's context, which provides access to the engine's services.
/// </summary>
private static IContext CONTEXT_INSTANCE;
/// <summary>
/// Sets the engine's context, allowing access to the engine's services.
/// </summary>
/// <param name="parContext">The context to use for the engine.</param>
internal static void SetContext(IContext parContext)
{
CONTEXT_INSTANCE = parContext;
} }
} }

View File

@@ -23,10 +23,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Engine\Engine.csproj"/> <ProjectReference Include="..\Engine\Engine.csproj"/>
</ItemGroup> <ProjectReference Include="..\TestUtil\TestUtil.csproj" />
<ItemGroup>
<Folder Include="src\"/>
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -68,4 +68,220 @@ public class GameObjectTests
Assert.Throws<ArgumentException>(() => _gameObject.ProcessChanges()); Assert.Throws<ArgumentException>(() => _gameObject.ProcessChanges());
} }
[Test]
public void AddComponent_ShouldSetGameObjectForAddedComponent()
{
var testComponent = new TestComponent();
_gameObject.AddComponent(testComponent);
_gameObject.ProcessChanges();
Assert.Multiple(() =>
{
Assert.That(testComponent.GameObject, Is.EqualTo(_gameObject));
});
}
[Test]
public void AddComponent_ShouldCallAwakeOnAddedComponentOnPreUpdate()
{
var testComponent = new TestComponent();
var wasAwakeCalled = false;
testComponent.OnAwake += () => wasAwakeCalled = true;
_gameObject.AddComponent(testComponent);
_gameObject.ProcessChanges();
_gameObject.PreUpdate(0);
Assert.Multiple(() =>
{
Assert.That(wasAwakeCalled, Is.True);
});
}
[Test]
public void AddComponent_ShouldCallStartOnAddedComponentOnPreUpdate()
{
var testComponent = new TestComponent();
var wasStartCalled = false;
testComponent.OnStart += () => wasStartCalled = true;
_gameObject.AddComponent(testComponent);
_gameObject.ProcessChanges();
_gameObject.PreUpdate(0);
Assert.Multiple(() =>
{
Assert.That(wasStartCalled, Is.True);
});
}
[Test]
public void AddComponent_ShouldCallPreUpdateOnAddedComponentOnPreUpdate()
{
var testComponent = new TestComponent();
var wasPreUpdateCalled = false;
testComponent.OnPreUpdate += _ => wasPreUpdateCalled = true;
_gameObject.AddComponent(testComponent);
_gameObject.ProcessChanges();
_gameObject.PreUpdate(0);
Assert.Multiple(() =>
{
Assert.That(wasPreUpdateCalled, Is.True);
});
}
[Test]
public void AddComponent_ShouldCallUpdateOnAddedComponentOnUpdate()
{
var testComponent = new TestComponent();
var wasUpdateCalled = false;
testComponent.OnUpdate += _ => wasUpdateCalled = true;
_gameObject.AddComponent(testComponent);
_gameObject.ProcessChanges();
_gameObject.Update(0);
Assert.Multiple(() =>
{
Assert.That(wasUpdateCalled, Is.True);
});
}
[Test]
public void AddComponent_ShouldCallPostUpdateOnAddedComponentOnPostUpdate()
{
var testComponent = new TestComponent();
var wasPostUpdateCalled = false;
testComponent.OnPostUpdate += _ => wasPostUpdateCalled = true;
_gameObject.AddComponent(testComponent);
_gameObject.ProcessChanges();
_gameObject.PostUpdate(0);
Assert.Multiple(() =>
{
Assert.That(wasPostUpdateCalled, Is.True);
});
}
[Test]
public void AddComponent_ShouldCallRenderOnAddedComponentOnRender()
{
var testComponent = new TestComponent();
var wasRenderCalled = false;
testComponent.OnRender += () => wasRenderCalled = true;
_gameObject.AddComponent(testComponent);
_gameObject.ProcessChanges();
_gameObject.Render();
Assert.Multiple(() =>
{
Assert.That(wasRenderCalled, Is.True);
});
}
[Test]
public void AddComponent_ShouldCallDestroyOnAddedComponentOnDestroy()
{
var testComponent = new TestComponent();
var wasDestroyCalled = false;
testComponent.OnDestroy += () => wasDestroyCalled = true;
_gameObject.AddComponent(testComponent);
_gameObject.ProcessChanges();
_gameObject.Destroy();
Assert.Multiple(() =>
{
Assert.That(wasDestroyCalled, Is.True);
});
}
[Test]
public void AddComponent_ShouldCallDisableOnAddedComponentOnDisable()
{
var testComponent = new TestComponent();
var wasDisableCalled = false;
testComponent.OnDisable += () => wasDisableCalled = true;
_gameObject.IsEnabled = false;
_gameObject.AddComponent(testComponent);
_gameObject.ProcessChanges();
_gameObject.Update(0);
Assert.Multiple(() =>
{
Assert.That(wasDisableCalled, Is.True);
});
}
[Test]
public void AddComponent_ShouldCallEnableOnAddedComponentOnEnable()
{
var testComponent = new TestComponent();
var wasEnableCalled = false;
testComponent.OnEnable += () => wasEnableCalled = true;
_gameObject.IsEnabled = false;
_gameObject.AddComponent(testComponent);
_gameObject.ProcessChanges();
_gameObject.Update(0);
_gameObject.IsEnabled = true;
_gameObject.ProcessChanges();
_gameObject.Update(0);
Assert.Multiple(() =>
{
Assert.That(wasEnableCalled, Is.True);
});
}
[Test]
public void RemoveComponent_ShouldThrowIfComponentIsTransform()
{
Assert.Throws<ArgumentException>(() => _gameObject.RemoveComponent<Transform>());
}
[Test]
public void RemoveComponent_ShouldThrowIfComponentDoesNotExist()
{
_gameObject.RemoveComponent<TestComponent>();
Assert.Throws<ArgumentException>(() => _gameObject.ProcessChanges());
}
[Test]
public void GetComponent_ShouldReturnComponentIfExists()
{
var testComponent = new TestComponent();
_gameObject.AddComponent(testComponent);
_gameObject.ProcessChanges();
var component = _gameObject.GetComponent<TestComponent>();
Assert.Multiple(() =>
{
Assert.That(component, Is.EqualTo(testComponent));
});
}
[Test]
public void GetComponent_ShouldReturnNullIfComponentDoesNotExist()
{
_gameObject.ProcessChanges();
var component = _gameObject.GetComponent<TestComponent>();
Assert.Multiple(() =>
{
Assert.That(component, Is.Null);
});
}
} }

View File

@@ -0,0 +1,199 @@
using Engine.Scene;
namespace EngineTests.Scene;
public class SceneTests
{
private Engine.Scene.Scene _scene;
[SetUp]
public void Setup()
{
_scene = new Engine.Scene.Scene();
}
[Test]
public void Enter_ShouldSetSceneToPlaying()
{
Assert.Multiple(() =>
{
Assert.That(_scene.IsPlaying, Is.False);
});
_scene.Enter();
Assert.Multiple(() =>
{
Assert.That(_scene.IsPlaying, Is.True);
});
}
[Test]
public void Exit_ShouldUnsetSceneToPlaying()
{
_scene.Enter();
Assert.Multiple(() =>
{
Assert.That(_scene.IsPlaying, Is.True);
});
_scene.Exit();
Assert.Multiple(() =>
{
Assert.That(_scene.IsPlaying, Is.False);
});
}
[Test]
public void Update_ShouldRespectTimeScale()
{
const double INITIAL_DELTA_TIME = 1.0;
const double TIME_SCALE = 2.0;
var (gameObject, testComponent) = CreateTestGameObject();
var actualDeltaTime = 0.0;
testComponent.OnUpdate += parDeltaTime => actualDeltaTime = parDeltaTime;
_scene.Add(gameObject);
_scene.Enter();
_scene.TimeScale = TIME_SCALE;
_scene.Update(INITIAL_DELTA_TIME);
Assert.Multiple(() =>
{
Assert.That(actualDeltaTime, Is.EqualTo(INITIAL_DELTA_TIME * TIME_SCALE));
});
}
[Test]
public void Add_ShouldSetSceneForAddedGameObject()
{
var (gameObject, _) = CreateTestGameObject();
_scene.Add(gameObject);
_scene.ProcessChanges();
Assert.Multiple(() =>
{
Assert.That(gameObject.Scene, Is.EqualTo(_scene));
});
}
[Test]
public void Add_ShouldAddGameObjectToHierarchy()
{
var (gameObject, _) = CreateTestGameObject();
_scene.Add(gameObject);
_scene.ProcessChanges();
Assert.Multiple(() =>
{
Assert.That(_scene.Hierarchy.Objects, Contains.Item(gameObject));
});
}
[Test]
public void Remove_ShouldUnsetSceneForRemovedGameObject()
{
var (gameObject, _) = CreateTestGameObject();
_scene.Add(gameObject);
_scene.ProcessChanges();
_scene.Remove(gameObject);
_scene.ProcessChanges();
Assert.Multiple(() =>
{
Assert.That(gameObject.Scene, Is.Null);
});
}
[Test]
public void Remove_ShouldRemoveGameObjectFromHierarchy()
{
var (gameObject, _) = CreateTestGameObject();
_scene.Add(gameObject);
_scene.ProcessChanges();
_scene.Remove(gameObject);
_scene.ProcessChanges();
Assert.Multiple(() =>
{
Assert.That(_scene.Hierarchy.Objects, Is.Not.Contains(gameObject));
});
}
[Test]
public void FindAllComponents_ShouldReturnAllComponentsCount1()
{
var (gameObject, testComponent) = CreateTestGameObject();
_scene.Add(gameObject);
_scene.ProcessChanges();
var components = _scene.FindAllComponents<TestComponent>();
Assert.Multiple(() =>
{
Assert.That(components, Contains.Item(testComponent));
Assert.That(components, Has.Count.EqualTo(1));
});
}
[Test]
public void FindAllComponents_ShouldReturnAllComponentsCount2()
{
var (gameObject1, testComponent1) = CreateTestGameObject();
var (gameObject2, testComponent2) = CreateTestGameObject();
_scene.Add(gameObject1);
_scene.Add(gameObject2);
_scene.ProcessChanges();
var components = _scene.FindAllComponents<TestComponent>();
Assert.Multiple(() =>
{
Assert.That(components, Contains.Item(testComponent1));
Assert.That(components, Contains.Item(testComponent2));
Assert.That(components, Has.Count.EqualTo(2));
});
}
[Test]
public void FindFirstComponent_ShouldReturnFirstComponent()
{
var (gameObject, testComponent) = CreateTestGameObject();
_scene.Add(gameObject);
_scene.ProcessChanges();
var component = _scene.FindFirstComponent<TestComponent>();
Assert.Multiple(() =>
{
Assert.That(component, Is.EqualTo(testComponent));
});
}
[Test]
public void FindFirstComponent_ShouldReturnNullIfComponentDoesNotExist()
{
_scene.ProcessChanges();
var component = _scene.FindFirstComponent<TestComponent>();
Assert.Multiple(() =>
{
Assert.That(component, Is.Null);
});
}
private static (GameObject, TestComponent) CreateTestGameObject()
{
var gameObject = new GameObject();
var testComponent = new TestComponent();
gameObject.AddComponent(testComponent);
gameObject.ProcessChanges();
return (gameObject, testComponent);
}
}

View File

@@ -0,0 +1,61 @@
using Engine.Scene.Component;
namespace EngineTests.Scene;
public class TestComponent : Component
{
public event Action? OnAwake;
public event Action? OnStart;
public event Action<double>? OnPreUpdate;
public event Action<double>? OnUpdate;
public event Action<double>? OnPostUpdate;
public event Action? OnRender;
public event Action? OnDestroy;
public event Action? OnEnable;
public event Action? OnDisable;
public override void Awake()
{
OnAwake?.Invoke();
}
public override void Start()
{
OnStart?.Invoke();
}
public override void PreUpdate(double parDeltaTime)
{
OnPreUpdate?.Invoke(parDeltaTime);
}
public override void Update(double parDeltaTime)
{
OnUpdate?.Invoke(parDeltaTime);
}
public override void PostUpdate(double parDeltaTime)
{
OnPostUpdate?.Invoke(parDeltaTime);
}
public override void Render()
{
OnRender?.Invoke();
}
public override void Destroy()
{
OnDestroy?.Invoke();
}
public override void Enable()
{
OnEnable?.Invoke();
}
public override void Disable()
{
OnDisable?.Invoke();
}
}

View File

@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0"/>
<PackageReference Include="NUnit" Version="3.14.0"/>
<PackageReference Include="NUnit.Analyzers" Version="3.9.0"/>
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0"/>
</ItemGroup>
<ItemGroup>
<Using Include="NUnit.Framework"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DoomDeathmatch\DoomDeathmatch.csproj" />
<ProjectReference Include="..\Engine\Engine.csproj" />
<ProjectReference Include="..\TestUtil\TestUtil.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,52 @@
using DoomDeathmatch;
using DoomDeathmatch.Component.MVC.Health;
using DoomDeathmatch.Component.MVC.Weapon;
namespace GameTests;
public class HealthControllerTest : SceneMockTest
{
[Test]
public void HealthController_HealthShouldBeCappedToMaxHealthWhenHealed()
{
const float MAX_HEALTH = 100;
var healthController = new HealthController(MAX_HEALTH);
var gameObject = GameObjectUtil.CreateGameObject(Scene, [
healthController
]);
Scene.Add(gameObject);
SceneManager.Update(1);
healthController.Heal(MAX_HEALTH * 2);
SceneManager.Update(1);
Assert.Multiple(() =>
{
Assert.That(healthController.Health, Is.EqualTo(MAX_HEALTH));
});
}
[Test]
public void HealthController_HealthShouldBeCappedToZeroWhenHealedBelowZero()
{
const float MAX_HEALTH = 100;
var healthController = new HealthController(MAX_HEALTH);
var gameObject = GameObjectUtil.CreateGameObject(Scene, [
healthController
]);
Scene.Add(gameObject);
SceneManager.Update(1);
healthController.TakeDamage(MAX_HEALTH * 2);
SceneManager.Update(1);
Assert.Multiple(() =>
{
Assert.That(healthController.Health, Is.EqualTo(0));
});
}
}

View File

@@ -0,0 +1,154 @@
using DoomDeathmatch;
using DoomDeathmatch.Component.MVC;
using DoomDeathmatch.Component.Physics;
using DoomDeathmatch.Component.Util;
using DoomDeathmatch.Scene.Play.Prefab;
using Engine.Input;
using OpenTK.Mathematics;
namespace GameTests;
public class MovementComponentTest : SceneMockTest
{
[Test]
public void PlayerMovement_ShouldMovePlayerLeftWithKeyboardA()
{
var playerMovementComponent = new PlayerMovementComponent();
var movementComponent = new MovementComponent();
var playerObject = GameObjectUtil.CreateGameObject(Scene, [
new RigidbodyComponent(),
movementComponent,
playerMovementComponent
]);
Scene.Add(playerObject);
SceneManager.Update(1);
InputHandler.PressedKeyboardButtons.Add(KeyboardButtonCode.A);
SceneManager.Update(1);
Assert.Multiple(() =>
{
Assert.That(playerObject.Transform.Translation.X, Is.EqualTo(-movementComponent.Speed));
});
}
[Test]
public void PlayerMovement_ShouldMovePlayerRightWithKeyboardD()
{
var playerMovementComponent = new PlayerMovementComponent();
var movementComponent = new MovementComponent();
var playerObject = GameObjectUtil.CreateGameObject(Scene, [
new RigidbodyComponent(),
movementComponent,
playerMovementComponent
]);
Scene.Add(playerObject);
SceneManager.Update(1);
InputHandler.PressedKeyboardButtons.Add(KeyboardButtonCode.D);
SceneManager.Update(1);
Assert.Multiple(() =>
{
Assert.That(playerObject.Transform.Translation.X, Is.EqualTo(movementComponent.Speed));
});
}
[Test]
public void PlayerMovement_ShouldMovePlayerUpWithKeyboardW()
{
var playerMovementComponent = new PlayerMovementComponent();
var movementComponent = new MovementComponent();
var playerObject = GameObjectUtil.CreateGameObject(Scene, [
new RigidbodyComponent(),
movementComponent,
playerMovementComponent
]);
Scene.Add(playerObject);
SceneManager.Update(1);
InputHandler.PressedKeyboardButtons.Add(KeyboardButtonCode.W);
SceneManager.Update(1);
Assert.Multiple(() =>
{
Assert.That(playerObject.Transform.Translation.Y, Is.EqualTo(movementComponent.Speed));
});
}
[Test]
public void PlayerMovement_ShouldMovePlayerDownWithKeyboardS()
{
var playerMovementComponent = new PlayerMovementComponent();
var movementComponent = new MovementComponent();
var playerObject = GameObjectUtil.CreateGameObject(Scene, [
new RigidbodyComponent(),
movementComponent,
playerMovementComponent
]);
Scene.Add(playerObject);
SceneManager.Update(1);
InputHandler.PressedKeyboardButtons.Add(KeyboardButtonCode.S);
SceneManager.Update(1);
Assert.Multiple(() =>
{
Assert.That(playerObject.Transform.Translation.Y, Is.EqualTo(-movementComponent.Speed));
});
}
[Test]
public void PlayerMovement_ShouldRotatePlayerRightWithKeyboardQ()
{
var playerMovementComponent = new PlayerMovementComponent();
var movementComponent = new MovementComponent();
var playerObject = GameObjectUtil.CreateGameObject(Scene, [
new RigidbodyComponent(),
movementComponent,
playerMovementComponent
]);
Scene.Add(playerObject);
SceneManager.Update(1);
InputHandler.PressedKeyboardButtons.Add(KeyboardButtonCode.Q);
SceneManager.Update(1);
Console.WriteLine(playerObject.Transform.Rotation.ToEulerAngles());
Assert.Multiple(() =>
{
Assert.That(playerObject.Transform.Rotation.ToEulerAngles().Z,
Is.EqualTo(MathHelper.DegreesToRadians(movementComponent.RotationSpeed)));
});
}
[Test]
public void PlayerMovement_ShouldRotatePlayerRightWithKeyboardE()
{
var playerMovementComponent = new PlayerMovementComponent();
var movementComponent = new MovementComponent();
var playerObject = GameObjectUtil.CreateGameObject(Scene, [
new RigidbodyComponent(),
movementComponent,
playerMovementComponent
]);
Scene.Add(playerObject);
SceneManager.Update(1);
InputHandler.PressedKeyboardButtons.Add(KeyboardButtonCode.E);
SceneManager.Update(1);
Assert.Multiple(() =>
{
Assert.That(playerObject.Transform.Rotation.ToEulerAngles().Z,
Is.EqualTo(-MathHelper.DegreesToRadians(movementComponent.RotationSpeed)));
});
}
}

View File

@@ -0,0 +1,51 @@
using Engine.Graphics;
using Engine.Graphics.Pipeline;
using Engine.Scene;
using TestUtil;
using TestUtil.Renderer;
namespace GameTests;
public abstract class SceneMockTest
{
public Scene Scene => _scene;
public MockContext Context => _context;
public MockInputHandler InputHandler => _inputHandler;
public MockResourceManager ResourceManager => _resourceManager;
public SceneManager SceneManager => _sceneManager;
public MockRenderer Renderer => _renderer;
private Scene _scene;
private MockContext _context;
private MockInputHandler _inputHandler;
private MockResourceManager _resourceManager;
private SceneManager _sceneManager;
private MockRenderer _renderer;
[SetUp]
public void SetUp()
{
_scene = new Scene();
_inputHandler = new MockInputHandler();
_resourceManager = new MockResourceManager();
_sceneManager = new SceneManager();
_renderer = new MockRenderer(new Dictionary<RenderLayer, IGenericRenderer>
{
[RenderLayer.DEFAULT] = new MockGenericRenderer(new MockQuadRenderer(), new MockAnyMeshRenderer(),
new MockTextRenderer()),
[RenderLayer.OVERLAY] = new MockGenericRenderer(new MockQuadRenderer(), new MockAnyMeshRenderer(),
new MockTextRenderer()),
[RenderLayer.HUD] = new MockGenericRenderer(new MockQuadRenderer(), new MockAnyMeshRenderer(),
new MockTextRenderer())
});
_context = new MockContext(_inputHandler, _resourceManager, _sceneManager, _renderer, "");
MockContext.SetMockContext(_context);
_sceneManager.TransitionTo(() => Scene);
_sceneManager.Update(0);
}
}

View File

@@ -0,0 +1,104 @@
using DoomDeathmatch;
using DoomDeathmatch.Component.MVC.Weapon;
using DoomDeathmatch.Script.Model.Weapon;
namespace GameTests;
public class WeaponControllerTest : SceneMockTest
{
[Test]
public void WeaponController_DefaultWeaponShouldBeSelected()
{
var weapon = WeaponData.Pistol;
var weaponController = new WeaponController(weapon);
var gameObject = GameObjectUtil.CreateGameObject(Scene, [
weaponController
]);
Scene.Add(gameObject);
SceneManager.Update(1);
weaponController.SelectWeapon(0);
SceneManager.Update(1);
Assert.Multiple(() =>
{
Assert.That(weaponController.WeaponData, Is.EqualTo(weapon));
});
}
[Test]
public void WeaponController_CannotRemoveDefaultWeapon()
{
var weapon = WeaponData.Pistol;
var weaponController = new WeaponController(weapon);
var gameObject = GameObjectUtil.CreateGameObject(Scene, [
weaponController
]);
Scene.Add(gameObject);
SceneManager.Update(1);
weaponController.RemoveWeapon(0);
SceneManager.Update(1);
Assert.Multiple(() =>
{
Assert.That(weaponController.WeaponData, Is.EqualTo(weapon));
});
}
[Test]
public void WeaponController_CannotSelectNonExistentWeapon()
{
var weapon = WeaponData.Pistol;
var weaponController = new WeaponController(weapon);
var gameObject = GameObjectUtil.CreateGameObject(Scene, [
weaponController
]);
Scene.Add(gameObject);
SceneManager.Update(1);
weaponController.SelectWeapon(1);
SceneManager.Update(1);
Assert.Multiple(() =>
{
Assert.That(weaponController.WeaponData, Is.EqualTo(weapon));
});
}
[Test]
public void WeaponController_ShouldAddAmmoOnWeaponMerge()
{
const int INITIAL_AMMO1 = 10;
const int INITIAL_AMMO2 = 10;
var weapon1 = WeaponData.Pistol;
var weapon2 = WeaponData.Pistol;
weapon1.Ammo = INITIAL_AMMO1;
weapon2.Ammo = INITIAL_AMMO2;
var weaponController = new WeaponController(weapon1);
var gameObject = GameObjectUtil.CreateGameObject(Scene, [
weaponController
]);
Scene.Add(gameObject);
SceneManager.Update(1);
Assert.Multiple(() =>
{
Assert.That(weaponController.WeaponData.Ammo, Is.EqualTo(INITIAL_AMMO1));
});
weaponController.AddOrMergeWeapon(weapon2);
SceneManager.Update(1);
Assert.Multiple(() =>
{
Assert.That(weaponController.WeaponData.Ammo, Is.EqualTo(INITIAL_AMMO1 + INITIAL_AMMO2));
});
}
}

View File

@@ -81,36 +81,36 @@ float perceptualColorDistance(vec3 color1, vec3 color2) {
return dot(delta, delta); return dot(delta, delta);
} }
//int findMostPerceptuallyAccurateColor(vec3 color) {
// int bestMatchIndex = 0;
// float minDistance = perceptualColorDistance(color, ConsoleColorVec3[0]);
//
// for (int i = 1; i < 16; i++) {
// float currentDistance = perceptualColorDistance(color, ConsoleColorVec3[i]);
// if (currentDistance < minDistance) {
// minDistance = currentDistance;
// bestMatchIndex = i;
// }
// }
//
// return bestMatchIndex;
//}
int findMostPerceptuallyAccurateColor(vec3 color) { int findMostPerceptuallyAccurateColor(vec3 color) {
int closestIndex = 0; int bestMatchIndex = 0;
float minDistance = distance(color, ConsoleColorVec3[0]); float minDistance = perceptualColorDistance(color, ConsoleColorVec3[0]);
for (int i = 1; i < 16; i++) { for (int i = 1; i < 16; i++) {
float dist = distance(color, ConsoleColorVec3[i]); float currentDistance = perceptualColorDistance(color, ConsoleColorVec3[i]);
if (dist < minDistance) { if (currentDistance < minDistance) {
minDistance = dist; minDistance = currentDistance;
closestIndex = i; bestMatchIndex = i;
} }
} }
return closestIndex; return bestMatchIndex;
} }
//int findMostPerceptuallyAccurateColor(vec3 color) {
// int closestIndex = 0;
// float minDistance = distance(color, ConsoleColorVec3[0]);
//
// for (int i = 1; i < 16; i++) {
// float dist = distance(color, ConsoleColorVec3[i]);
// if (dist < minDistance) {
// minDistance = dist;
// closestIndex = i;
// }
// }
//
// return closestIndex;
//}
// Enhanced luminosity calculation considering human perception // Enhanced luminosity calculation considering human perception
float calculatePerceptualLuminance(vec3 color) { float calculatePerceptualLuminance(vec3 color) {
// BT.709 luminance coefficients with slight adjustment // BT.709 luminance coefficients with slight adjustment

13
TestUtil/TestUtil.csproj Normal file
View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Engine\Engine.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,40 @@
using Engine.Context;
using Engine.Graphics;
using Engine.Input;
using Engine.Resource;
using Engine.Scene;
using Engine.Util;
namespace TestUtil;
public class MockContext : IContext
{
public Action? OnClose;
public IInputHandler InputHandler { get; }
public IResourceManager AssetResourceManager { get; }
public ISceneManager SceneManager { get; }
public IRenderer Renderer { get; }
public string DataFolder { get; }
public MockContext(IInputHandler parInputHandler, IResourceManager parAssetResourceManager,
ISceneManager parSceneManager, IRenderer parRenderer,
string parDataFolder)
{
InputHandler = parInputHandler;
AssetResourceManager = parAssetResourceManager;
SceneManager = parSceneManager;
Renderer = parRenderer;
DataFolder = parDataFolder;
}
public static void SetMockContext(MockContext parContext)
{
EngineUtil.SetContext(parContext);
}
public void Close()
{
OnClose?.Invoke();
}
}

View File

@@ -0,0 +1,46 @@
using System.Globalization;
using Engine.Input;
using OpenTK.Mathematics;
namespace TestUtil;
public class MockInputHandler : IInputHandler
{
public CultureInfo CurrentInputLanguage { get; set; } = new(1033);
public Vector2 MousePosition { get; set; } = Vector2.Zero;
public ISet<KeyboardButtonCode> PressedKeyboardButtons => _pressedKeyboardButtons;
public ISet<KeyboardButtonCode> JustPressedKeyboardButtons => _justPressedKeyboardButtons;
public ISet<MouseButtonCode> PressedMouseButtons => _pressedMouseButtons;
public ISet<MouseButtonCode> JustPressedMouseButtons => _justPressedMouseButtons;
private readonly HashSet<KeyboardButtonCode> _pressedKeyboardButtons = [];
private readonly HashSet<KeyboardButtonCode> _justPressedKeyboardButtons = [];
private readonly HashSet<MouseButtonCode> _pressedMouseButtons = [];
private readonly HashSet<MouseButtonCode> _justPressedMouseButtons = [];
public void Update(double parDeltaTime)
{
}
public bool IsKeyPressed(KeyboardButtonCode parKeyboardButtonCode)
{
return _pressedKeyboardButtons.Contains(parKeyboardButtonCode);
}
public bool IsKeyJustPressed(KeyboardButtonCode parKeyboardButtonCode)
{
return _justPressedKeyboardButtons.Contains(parKeyboardButtonCode);
}
public bool IsMouseButtonPressed(MouseButtonCode parButtonCode)
{
return _pressedMouseButtons.Contains(parButtonCode);
}
public bool IsMouseButtonJustPressed(MouseButtonCode parButtonCode)
{
return _justPressedMouseButtons.Contains(parButtonCode);
}
}

View File

@@ -0,0 +1,15 @@
using Engine.Resource;
namespace TestUtil;
public class MockResourceManager : IResourceManager
{
public event Action<Type, string>? OnLoad;
public T Load<T>(string parPath) where T : class
{
OnLoad?.Invoke(typeof(T), parPath);
return null!;
}
}

View File

@@ -0,0 +1,16 @@
using Engine.Asset.Mesh;
using Engine.Graphics.Render.Mesh;
using Engine.Graphics.Texture;
using OpenTK.Mathematics;
namespace TestUtil.Renderer;
public class MockAnyMeshRenderer : IAnyMeshRenderer
{
public event Action<Mesh, Matrix4, Texture?>? OnCommit;
public void Commit(Mesh parMesh, in Matrix4 parModelMatrix, Texture? parAlbedo = null)
{
OnCommit?.Invoke(parMesh, parModelMatrix, parAlbedo);
}
}

View File

@@ -0,0 +1,21 @@
using Engine.Graphics;
using Engine.Graphics.Render.Mesh;
using Engine.Graphics.Render.Quad;
using Engine.Graphics.Render.Text;
namespace TestUtil.Renderer;
public class MockGenericRenderer : IGenericRenderer
{
public IQuadRenderer QuadRenderer { get; }
public IAnyMeshRenderer AnyMeshRenderer { get; }
public ITextRenderer TextRenderer { get; }
public MockGenericRenderer(IQuadRenderer parQuadRenderer, IAnyMeshRenderer parAnyMeshRenderer,
ITextRenderer parTextRenderer)
{
QuadRenderer = parQuadRenderer;
AnyMeshRenderer = parAnyMeshRenderer;
TextRenderer = parTextRenderer;
}
}

View File

@@ -0,0 +1,15 @@
using Engine.Graphics.Render.Quad;
using Engine.Graphics.Texture;
using OpenTK.Mathematics;
namespace TestUtil.Renderer;
public class MockQuadRenderer : IQuadRenderer
{
public event Action<Matrix4, Vector4, Texture?>? OnCommit;
public void Commit(in Matrix4 parModelMatrix, in Vector4 parColor, Texture? parTexture = null)
{
OnCommit?.Invoke(parModelMatrix, parColor, parTexture);
}
}

View File

@@ -0,0 +1,16 @@
using Engine.Graphics;
using Engine.Graphics.Pipeline;
namespace TestUtil.Renderer;
public class MockRenderer : IRenderer
{
public IGenericRenderer this[RenderLayer parRenderLayer] => _renderers[parRenderLayer];
private readonly Dictionary<RenderLayer, IGenericRenderer> _renderers;
public MockRenderer(Dictionary<RenderLayer, IGenericRenderer> parRenderers)
{
_renderers = parRenderers;
}
}

View File

@@ -0,0 +1,15 @@
using Engine.Asset.Font;
using Engine.Graphics.Render.Text;
using OpenTK.Mathematics;
namespace TestUtil.Renderer;
public class MockTextRenderer : ITextRenderer
{
public event Action<Font, string, Vector4, Matrix4>? OnCommit;
public void Commit(Font parFont, string parText, in Vector4 parColor, in Matrix4 parModelMatrix)
{
OnCommit?.Invoke(parFont, parText, parColor, parModelMatrix);
}
}