Files
Voile/Voile/Source/Game.cs
2025-06-06 22:40:16 +02:00

259 lines
7.8 KiB
C#

using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Voile.Audio;
using Voile.Input;
using Voile.Rendering;
using Voile.Resources;
using Voile.VFS;
namespace Voile
{
public interface IGame
{
string Name { get; }
/// <summary>
/// Resource root path for this game.
/// </summary>
string ResourceRoot { get; }
void Start();
void Run();
void Shutdown();
}
/// <summary>
/// Entry point for the Voile game.
/// </summary>
public abstract class Game : IGame
{
public double UpdateTimeStep { get; set; } = 1.0 / 60.0;
public TimeSpan UpdateFrameTime { get; private set; }
public TimeSpan RenderFrameTime { get; private set; }
/// <summary>
/// The ResourceManager associated with this game.
/// </summary>
protected ResourceManager ResourceManager { get; private set; } = new();
/// <summary>
/// The InputHandler associated with this game. Uses <see cref="RaylibInputSystem"/> by default.
/// </summary>
protected InputSystem? Input { get; set; }
/// <summary>
/// The Renderer associated with this game. Uses <see cref="RaylibRenderSystem"/> by default.
/// </summary>
protected RenderSystem? Renderer { get; set; }
protected AudioSystem? AudioSystem { get; set; }
/// <summary>
/// Name of this game. Also used as a default window title.
/// </summary>
public virtual string Name => "Voile Game";
public virtual string ResourceRoot => "Resources/";
/// <summary>
/// Default game constructor.
/// </summary>
public Game()
{
ResourceManager = new ResourceManager();
Initialize();
}
/// <summary>
/// Game constructor with custom systems.
/// </summary>
/// <param name="inputSystem"></param>
/// <param name="renderSystem"></param>
public Game(InputSystem inputSystem, RenderSystem renderSystem)
{
Input = inputSystem;
Renderer = renderSystem;
}
/// <summary>
/// Starts the game application.
/// This involves initializing the required subsystems, loading resources from the disk, and then preparing the subsystems for running in the main loop.
/// </summary>
public void Start()
{
if (Renderer is null)
{
throw new NullReferenceException("No renderer provided.");
}
if (Input is null)
{
throw new NullReferenceException("No input system provided.");
}
if (ResourceManager is null)
{
throw new NullReferenceException("No ResourceManager provided.");
}
Mount();
LoadResources();
Ready();
Run();
Shutdown();
}
/// <summary>
/// Initializes systems using default types and settings.
/// </summary>
public void InitializeSystemsDefault()
{
ResourceManager.ResourceRoot = ResourceRoot;
if (Renderer is null)
{
Renderer = new RaylibRenderSystem();
}
if (Input is null)
{
Input = new RaylibInputSystem();
}
Input.Start();
InitializeRenderer();
}
public void ShutdownDefault()
{
Input?.Dispose();
Renderer?.Dispose();
ResourceManager.Dispose();
}
/// <summary>
/// Adds a system that will start together with the game.
/// </summary>
/// <param name="system"></param>
public void AddSystem([DisallowNull] IStartableSystem system)
{
_startableSystems.Add(system);
}
/// <summary>
/// Adds a system to the Update step.
/// </summary>
/// <param name="system"></param>
public void AddSystemToUpdate([DisallowNull] IUpdatableSystem system)
{
_updatableSystems.Add(system);
}
/// <summary>
/// Adds a system to the render step.
/// </summary>
/// <param name="system"></param>
public void AddSystemToRender([DisallowNull] IUpdatableSystem system)
{
_renderableSystems.Add(system);
}
/// <summary>
/// Called when everything has been readied to start the main loop.
/// </summary>
public void Run()
{
if (Renderer is null)
{
throw new NullReferenceException("No renderer provided.");
}
Stopwatch stopwatch = Stopwatch.StartNew();
double previousTime = stopwatch.Elapsed.TotalSeconds;
while (Renderer.ShouldRun)
{
double currentTime = stopwatch.Elapsed.TotalSeconds;
double elapsedTime = currentTime - previousTime;
previousTime = currentTime;
_accumulator += elapsedTime;
while (_accumulator >= UpdateTimeStep)
{
foreach (var system in _updatableSystems)
{
system.Update(UpdateTimeStep);
}
Update(UpdateTimeStep);
_accumulator -= UpdateTimeStep;
}
Renderer.BeginFrame();
foreach (var system in _renderableSystems)
{
system.Update(Renderer.FrameTime);
}
Render(Renderer.FrameTime);
Renderer.EndFrame();
RenderFrameTime = TimeSpan.FromSeconds(Renderer.FrameTime);
}
}
/// <summary>
/// Called when it's time to initialize the subsystems, or modify default game settings or system settings.
/// </summary>
public abstract void Initialize();
/// <summary>
/// Called when it's safe to manipulate the resources or/and systems.
/// You can safely create game objects, scenes, etc. in this method.
/// </summary>
protected abstract void Ready();
/// <summary>
/// Called when it's time to load the game's resources, such as images or sounds.
/// </summary>
protected abstract void LoadResources();
/// <summary>
/// Triggered on each fixed timestep. Update your game's state here.
/// </summary>
/// <param name="deltaTime"></param>
protected abstract void Update(double deltaTime);
/// <summary>
/// Triggered when the renderer is ready to render the next frame.
/// </summary>
/// <param name="deltaTime"></param>
protected abstract void Render(double deltaTime);
/// <summary>
/// Called when the application quits and it's safe to clean up.
/// </summary>
public abstract void Shutdown();
private void InitializeRenderer()
{
var windowSettings = WindowSettings.Default;
if (!string.IsNullOrWhiteSpace(Name))
windowSettings.Title = Name;
var renderSettings = RendererSettings.Default;
renderSettings.WindowSettings = windowSettings;
Renderer?.Start(renderSettings);
}
private void Mount()
{
var resourceRootMount = new FileSystemMountPoint(ResourceRoot);
VirtualFileSystem.Mount(resourceRootMount);
}
private List<IStartableSystem> _startableSystems = new();
private List<IUpdatableSystem> _updatableSystems = new();
private List<IUpdatableSystem> _renderableSystems = new();
private double _accumulator;
}
}