Files
EightBitNet/Gaming/Game.cs
T

342 lines
12 KiB
C#

namespace Gaming
{
using EightBit;
using SDL3;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
public abstract class Game(bool verbose = false) : Device
{
private readonly Wrapper _wrapper = new(verbose);
private readonly SDL.PixelFormat _pixelType = SDL.PixelFormat.ARGB8888;
private bool _vsync;
private ulong _performanceFrequency;
private double _targetFrameTime;
private ulong _frameStartTime;
private ulong _frameEndTime;
private readonly SortedDictionary<uint, GameController> _gameControllers = [];
protected ScopedHandle Window { get; } = new(SDL.DestroyWindow);
protected ScopedHandle Renderer { get; } = new(SDL.DestroyRenderer);
protected ScopedHandle BitmapTexture { get; } = new(SDL.DestroyTexture);
protected IntPtr PixelFormat { get; private set; } = IntPtr.Zero;
public abstract float FramesPerSecond { get; }
public abstract bool UseVSYNC { get; }
protected virtual int WindowWidth => this.DisplayWidth * this.DisplayScale;
protected virtual int WindowHeight => this.DisplayHeight * this.DisplayScale;
public virtual int DisplayWidth => this.RasterWidth;
public virtual int DisplayHeight => this.RasterHeight;
public abstract int DisplayScale { get; }
public abstract int RasterWidth { get; }
public abstract int RasterHeight { get; }
public abstract string Title { get; }
protected abstract uint[] Pixels();
public override void RaisePOWER()
{
base.RaisePOWER();
this._wrapper.RaisePOWER();
this.Initialise();
}
public override void LowerPOWER()
{
this.Terminate();
this._wrapper.LowerPOWER();
base.LowerPOWER();
}
public virtual void Initialise()
{
this.Window.Handle = SDL.CreateWindow(this.Title, this.WindowWidth, this.WindowHeight, 0L);
Wrapper.MaybeThrowException(this.Window, "Unable to create window");
var displayId = SDL.GetDisplayForWindow(this.Window);
Wrapper.MaybeThrowException(displayId != 0, "Unable to obtain display ID for window");
var currentDisplayMode = SDL.GetCurrentDisplayMode(displayId);
Wrapper.MaybeThrowException(currentDisplayMode.HasValue, "Unable to obtain window mode");
this.Renderer.Handle = SDL.CreateRenderer(this.Window, null);
Wrapper.MaybeThrowException(this.Renderer, "Unable to create renderer: ");
this._vsync = this.UseVSYNC;
if (this._vsync)
{
var framesPerSecond = this.FramesPerSecond;
var refreshRate = currentDisplayMode?.RefreshRate;
Debug.Assert(refreshRate.HasValue, "refresh rate is unavailable");
this._vsync = Math.Abs(framesPerSecond - refreshRate.Value) < 0.001;
if (this._vsync)
{
SDL.LogInfo(SDL.LogCategory.Render, "Attempting to configure renderer VSYNC");
this._vsync = SDL.SetRenderVSync(this.Renderer, 1);
if (!this._vsync)
SDL.LogWarn(SDL.LogCategory.Render, $"Unable to set render VSYNC ({SDL.GetError()})");
}
else
{
SDL.LogWarn(SDL.LogCategory.Render, $"Display refresh rate is incompatible with required rate ({this.FramesPerSecond})");
}
}
this.PixelFormat = SDL.GetPixelFormatDetails(this._pixelType);
Wrapper.MaybeThrowException(this.PixelFormat, "Unable to obtain pixel format details");
this.ConfigureBackground();
this.CreateBitmapTexture();
this._performanceFrequency = SDL.GetPerformanceFrequency();
this._targetFrameTime = 1.0 / (double)this.FramesPerSecond;
}
public virtual void Terminate()
{
this.BitmapTexture.Dispose();
this.Renderer.Dispose();
this.Window.Dispose();
}
private void ConfigureBackground()
{
var success = SDL.SetRenderDrawColor(this.Renderer, 0, 0, 0, byte.MaxValue);
Wrapper.MaybeThrowException(success, "Unable to set render draw colour");
}
private void CreateBitmapTexture()
{
BitmapTexture.Handle = SDL.CreateTexture(this.Renderer, this._pixelType, SDL.TextureAccess.Streaming, this.RasterWidth, this.RasterHeight);
Wrapper.MaybeThrowException(BitmapTexture, "Unable to create bitmap texture");
}
public virtual void RunLoop()
{
while (this.Powered)
{
this.RunFrame();
}
}
public virtual void RunFrame()
{
this.Update();
this.HandleEvents();
if (this.Powered)
{
this.Draw();
_ = this.MaybeSynchronise();
}
}
protected virtual void Update()
{
this._frameStartTime = SDL.GetPerformanceCounter();
this.RunVerticalBlank();
this.RunRasterLines();
}
protected virtual void RunRasterLines()
{
}
protected virtual void RunVerticalBlank()
{
}
protected virtual void HandleEvents()
{
while (SDL.PollEvent(out var e))
{
this.HandleEvent(e);
}
}
protected virtual void HandleEvent(SDL.Event e)
{
switch ((SDL.EventType)e.Type)
{
case SDL.EventType.Quit:
this.LowerPOWER();
break;
case SDL.EventType.KeyDown:
_ = this.HandleKeyDown(e.Key.Key);
break;
case SDL.EventType.KeyUp:
_ = this.HandleKeyUp(e.Key.Key);
break;
case SDL.EventType.JoystickButtonDown:
_ = this.HandleJoyButtonDown(e.JButton);
break;
case SDL.EventType.JoystickButtonUp:
_ = this.HandleJoyButtonUp(e.JButton);
break;
case SDL.EventType.GamepadButtonDown:
_ = this.HandleGamepadButtonDown(e.GButton);
break;
case SDL.EventType.GamepadButtonUp:
_ = this.HandleGamepadButtonUp(e.GButton);
break;
case SDL.EventType.GamepadAdded:
this.AddGampad(e);
break;
case SDL.EventType.GamepadRemoved:
this.RemoveGamepad(e);
break;
}
}
protected virtual void Draw()
{
this.UpdateTexture();
this.RenderTexture();
this.DisplayTexture();
}
protected bool MaybeSynchronise()
{
var synchronise = !this._vsync;
if (synchronise)
this.Synchronise();
return synchronise;
}
protected void Synchronise()
{
this._frameEndTime = SDL.GetPerformanceCounter();
double frameTimeTicks = this._frameEndTime - this._frameStartTime;
SDL.LogDebug(SDL.LogCategory.Render, $"Frame time (ticks): {frameTimeTicks}");
var frameTime = frameTimeTicks / this._performanceFrequency;
SDL.LogDebug(SDL.LogCategory.Render, $"Frame time (seconds): {frameTime}");
var gap = this._targetFrameTime - frameTime;
SDL.LogDebug(SDL.LogCategory.Render, $"Timing gap (seconds): {gap}");
if (gap > 0.0)
{
var delay = (uint)(gap * 1000.0);
SDL.LogDebug(SDL.LogCategory.Render, $"Delay (ticks): {delay}");
SDL.Delay(delay);
}
if (gap < 0.0)
SDL.LogWarn(SDL.LogCategory.Render, "Running slowly");
}
protected virtual void RemoveGamepad(SDL.Event e)
{
var which = e.JDevice.Which;
var found = this._gameControllers.TryGetValue(which, out var gameController);
Debug.Assert(found);
Debug.Assert(gameController != null, "controller is not null");
_ = this._gameControllers.Remove(which);
SDL.LogInfo(SDL.LogCategory.Input, $"Joystick device {which} removed ({this._gameControllers.Count} controllers available)");
}
protected virtual void AddGampad(SDL.Event e)
{
var which = e.JDevice.Which;
var found = this._gameControllers.ContainsKey(which);
Debug.Assert(!found);
GameController gameController = new(which);
this._gameControllers[which] = gameController;
SDL.LogInfo(SDL.LogCategory.Input, $"Joystick device {which} address ({this._gameControllers.Count} controllers available)");
}
public GameController Gamepad(uint which)
{
var success = this._gameControllers.TryGetValue(which, out var gameController);
Wrapper.MaybeThrowException(success, "Unknown controller");
Debug.Assert(gameController != null, "controller is not null");
return gameController;
}
public uint ChooseControllerIndex(int who)
{
int count = this._gameControllers.Count;
int num;
switch (count)
{
case 0:
return uint.MaxValue;
case 1:
num = 1;
break;
default:
num = who == 1 ? 1 : 0;
break;
}
return num != 0 ? this._gameControllers.First().Key : count > 1 ? this._gameControllers.Skip(1).First().Key : uint.MaxValue;
}
public GameController? ChooseController(int who)
{
var key = this.ChooseControllerIndex(who);
if (key == uint.MaxValue)
return null;
var found = this._gameControllers.TryGetValue(key, out var gameController);
Debug.Assert(found);
return gameController;
}
protected void UpdateTexture()
{
var span = MemoryMarshal.Cast<uint, byte>(this.Pixels().AsSpan<uint>());
var success = SDL.UpdateTexture(this.BitmapTexture, IntPtr.Zero, span, this.DisplayWidth * sizeof(uint));
Wrapper.MaybeThrowException(success, "Unable to update texture");
}
protected void RenderTexture()
{
var success = SDL.RenderTexture(this.Renderer, this.BitmapTexture, IntPtr.Zero, IntPtr.Zero);
Wrapper.MaybeThrowException(success, "Unable to render texture");
}
protected void DisplayTexture()
{
var success = SDL.RenderPresent(this.Renderer);
Wrapper.MaybeThrowException(success, "Unable to present render to screen");
}
protected void ToggleFullscreen()
{
var fullscreen = (SDL.GetWindowFlags(this.Window) & SDL.WindowFlags.Fullscreen) > 0L;
var success = SDL.SetWindowFullscreen(this.Window, !fullscreen);
Wrapper.MaybeThrowException(success, "Failed to toggle window full screen setting");
Wrapper.MaybeThrowException(success ? SDL.ShowCursor() : SDL.HideCursor(), "Failed to toggle cursor show/hide");
}
protected virtual bool HandleKeyDown(SDL.Keycode key) => key == SDL.Keycode.F12;
protected virtual bool HandleKeyUp(SDL.Keycode key)
{
if (key != SDL.Keycode.F12)
return false;
this.ToggleFullscreen();
return true;
}
protected virtual bool HandleJoyButtonDown(SDL.JoyButtonEvent e) => false;
protected virtual bool HandleJoyButtonUp(SDL.JoyButtonEvent e) => false;
protected virtual bool HandleGamepadButtonDown(SDL.GamepadButtonEvent e) => false;
protected virtual bool HandleGamepadButtonUp(SDL.GamepadButtonEvent e) => false;
}
}