mirror of
https://github.com/MoleskiCoder/EightBitNet.git
synced 2026-10-22 02:02:24 +00:00
299 lines
11 KiB
C#
299 lines
11 KiB
C#
namespace Gaming
|
|
{
|
|
using EightBit;
|
|
using SDL3;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Runtime.InteropServices;
|
|
|
|
public abstract class Game : Device
|
|
{
|
|
private readonly Wrapper _wrapper;
|
|
private bool _vsync;
|
|
|
|
private readonly SortedDictionary<uint, GameController> _gameControllers = [];
|
|
|
|
public ILogger Logger => this._wrapper.Logger;
|
|
|
|
protected ScopedHandle Window { get; } = new(SDL.DestroyWindow);
|
|
|
|
protected ScopedHandle Renderer { get; } = new(SDL.DestroyRenderer);
|
|
|
|
protected ScopedHandle BitmapTexture { get; } = new(SDL.DestroyTexture);
|
|
|
|
protected abstract SDL.PixelFormat PixelFormat { get; }
|
|
|
|
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();
|
|
|
|
protected Game(ILogger.LogLevel logging)
|
|
{
|
|
this._wrapper = new(logging);
|
|
this.RaisingPOWER += this.Game_RaisingPOWER;
|
|
this.LoweredPOWER += this.Game_LoweredPOWER;
|
|
}
|
|
|
|
private void Game_RaisingPOWER(object? sender, EventArgs e)
|
|
{
|
|
this.Initialise();
|
|
}
|
|
|
|
private void Game_LoweredPOWER(object? sender, EventArgs e)
|
|
{
|
|
this.Terminate();
|
|
}
|
|
|
|
public virtual void Initialise()
|
|
{
|
|
this.Logger.Context = this.Title;
|
|
this.Logger.Verbosity = this._wrapper.Verbosity;
|
|
|
|
this._wrapper.RaisePOWER();
|
|
|
|
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)
|
|
{
|
|
this.Logger.Inform("Attempting to configure renderer VSYNC");
|
|
this._vsync = SDL.SetRenderVSync(this.Renderer, 1);
|
|
if (!this._vsync)
|
|
this.Logger.Warn($"Unable to set render VSYNC ({SDL.GetError()})");
|
|
}
|
|
else
|
|
{
|
|
this.Logger.Warn($"Display refresh rate is incompatible with required rate ({this.FramesPerSecond})");
|
|
}
|
|
}
|
|
|
|
if (!this._vsync)
|
|
{
|
|
this.Logger.Inform("Setting callback rate hint");
|
|
var success = SDL.SetHint("SDL_MAIN_CALLBACK_RATE", this.FramesPerSecond.ToString(CultureInfo.InvariantCulture));
|
|
Wrapper.MaybeThrowException(success, "Unable to set event loop callback rate hint");
|
|
}
|
|
|
|
this.ConfigureBackground();
|
|
this.CreateBitmapTexture();
|
|
}
|
|
|
|
public virtual void Terminate()
|
|
{
|
|
this.BitmapTexture.Dispose();
|
|
this.Renderer.Dispose();
|
|
this.Window.Dispose();
|
|
this._wrapper.LowerPOWER();
|
|
}
|
|
|
|
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.PixelFormat, SDL.TextureAccess.Streaming, this.RasterWidth, this.RasterHeight);
|
|
Wrapper.MaybeThrowException(BitmapTexture, "Unable to create bitmap texture");
|
|
}
|
|
|
|
public virtual SDL.AppResult RunFrame()
|
|
{
|
|
this.Update();
|
|
this.Draw();
|
|
return SDL.AppResult.Continue;
|
|
}
|
|
|
|
protected virtual void Update()
|
|
{
|
|
this.RunVerticalBlank();
|
|
this.RunRasterLines();
|
|
}
|
|
|
|
protected virtual void RunRasterLines()
|
|
{
|
|
}
|
|
|
|
protected virtual void RunVerticalBlank()
|
|
{
|
|
}
|
|
|
|
public virtual SDL.AppResult HandleEvent(SDL.Event e)
|
|
{
|
|
switch ((SDL.EventType)e.Type)
|
|
{
|
|
case SDL.EventType.Quit:
|
|
return SDL.AppResult.Success;
|
|
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;
|
|
}
|
|
return SDL.AppResult.Continue;
|
|
}
|
|
|
|
protected virtual void Draw()
|
|
{
|
|
this.UpdateTexture();
|
|
this.RenderTexture();
|
|
this.DisplayTexture();
|
|
}
|
|
|
|
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);
|
|
this.Logger.Inform($"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(this.Logger, which);
|
|
this._gameControllers[which] = gameController;
|
|
this.Logger.Inform($"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;
|
|
}
|
|
}
|