An SDL wrapper library for .NET.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

515 lines
22 KiB

using DotSDL.Events;
using DotSDL.Input;
using DotSDL.Interop.Core;
using System;
using System.Collections.Generic;
using System.Linq;
namespace DotSDL.Graphics {
/// <summary>
/// Represents an SDL window.
/// </summary>
public class SdlWindow : IResourceObject {
private readonly SdlInit _sdlInit = SdlInit.Instance;
private readonly ResourceManager _resources = ResourceManager.Instance;
private string _windowTitle;
private readonly IntPtr _window;
private readonly IntPtr _renderer;
private IntPtr _texture;
private bool _hasTexture;
private bool _running;
private uint _nextVideoUpdate;
private uint _nextGameUpdate;
private ScalingQuality _scalingQuality = ScalingQuality.Nearest;
/// <summary>Gets the background layer of this window. This is equivalent to accessing Layers[0].</summary>
public Canvas Background => Layers[0];
/// <summary>Gets the list of background layers for this window.</summary>
public List<Canvas> Layers { get; }
/// <summary>
/// Gets the number of background layers for this windows. This number will always be greater
/// than or equal to 1.
/// </summary>
public int LayerCount => Layers.Count;
/// <summary><c>true</c> if this <see cref="SdlWindow"/> instance has been destroyed, othersize <c>false</c>.</summary>
public bool IsDestroyed { get; set; }
/// <summary><c>true</c> if this <see cref="SdlWindow"/> has been minimized, othersize <c>false</c>.</summary>
public bool IsMinimized { get; set; }
public ResourceType ResourceType => ResourceType.Window;
/// <summary>Gets the width of this <see cref="SdlWindow"/>.</summary>
public int WindowWidth { get; }
/// <summary>Gets the height of this <see cref="SdlWindow"/>.</summary>
public int WindowHeight { get; }
/// <summary>Gets or sets the title of this <see cref="SdlWindow"/>.</summary>
public string WindowTitle {
get => _windowTitle;
set {
_windowTitle = value;
Video.SetWindowTitle(_window, _windowTitle);
}
}
/// <summary>Gets the width of the rendering target used by this <see cref="SdlWindow"/>.</summary>
public int RenderWidth { get; }
/// <summary>Gets the height of the rendering target used by this <see cref="SdlWindow"/>.</summary>
public int RenderHeight { get; }
/// <summary>The amount of time, in milliseconds, from when the application was started.</summary>
public uint TicksElapsed => Timer.GetTicks();
/// <summary>Gets or sets the amount of time, in milliseconds, between video updates.</summary>
public uint VideoUpdateTicks { get; set; }
/// <summary>Gets or sets the amount of time, in milliseconds, between game (logic) updates.</summary>
public uint GameUpdateTicks { get; set; }
/// <summary>Gets a <see cref="Rectangle"/> that can be manipulated to modify how much of the scene is displayed.</summary>
public Rectangle CameraView { get; }
/// <summary>The list of active <see cref="Sprite"/> objects.</summary>
public SpriteList Sprites { get; }
/// <summary>Indicates that the window manager should position the window. To place the window on a specific display, use the <see cref="WindowPosCenteredDisplay"/> function.</summary>
public const int WindowPosUndefined = 0x1FFF0000;
/// <summary>Fired when tboolhe window's close button is clicked.</summary>
public event EventHandler<WindowEvent> Closed;
/// <summary>Fired when a key is pressed.</summary>
public event EventHandler<KeyboardEvent> KeyPressed;
/// <summary>Fired when a key is released.</summary>
public event EventHandler<KeyboardEvent> KeyReleased;
/// <summary>Fired when the window's minimize button is clicked.</summary>
public event EventHandler<WindowEvent> Minimized;
/// <summary>Fired when the window is restored.</summary>
public event EventHandler<WindowEvent> Restored;
public ScalingQuality ScalingQuality {
get => _scalingQuality;
set {
_scalingQuality = value;
if(_hasTexture)
CreateTexture();
}
}
/// <summary>
/// Calculates a value that allows the window to be placed on a specific display, with its exact position determined by the window manager.
/// </summary>
/// <param name="display">The index of the display to place the window on.</param>
/// <returns>A coordinate value that should be passed to the <see cref="SdlWindow"/> constructor.</returns>
public static int WindowPosUndefinedDisplay(uint display) {
return (int)(WindowPosUndefined | display);
}
/// <summary>
/// Indicates that the window should be in the center of the screen. To center the window on a specific display, use the <see cref="WindowPosCenteredDisplay"/> function.
/// </summary>
public const int WindowPosCentered = 0x2FFF0000;
/// <summary>
/// Calculates a value that allows the window to be placed in the center of a specified display.
/// </summary>
/// <param name="display">The index of the display to place the window on.</param>
/// <returns>A coordinate value that should be passed to the <see cref="SdlWindow"/> constructor.</returns>
public static int WindowPosCenteredDisplay(uint display) {
return (int)(WindowPosCentered | display);
}
/// <summary>
/// Creates a new <see cref="SdlWindow"/>.readonly
/// </summary>
/// <param name="title">The text that is displayed on the window's title bar.</param>
/// <param name="position">A <see cref="Point"/> representing the starting position of the window. The X and Y coordinates of the Point can be set to <see cref="WindowPosUndefined"/> or <see cref="WindowPosCentered"/>.</param>
/// <param name="windowWidth">The width of the window.</param>
/// <param name="windowHeight">The height of the window.</param>
public SdlWindow(string title, Point position, int windowWidth, int windowHeight) : this(title, position, windowWidth, windowHeight, windowWidth, windowHeight, ScalingQuality.Nearest) { }
/// <summary>
/// Creates a new <see cref="SdlWindow"/>.
/// </summary>
/// <param name="title">The text that is displayed on the window's title bar.</param>
/// <param name="position">A <see cref="Point"/> representing the starting position of the window. The X and Y coordinates of the Point can be set to <see cref="WindowPosUndefined"/> or <see cref="WindowPosCentered"/>.</param>
/// <param name="windowWidth">The width of the window.</param>
/// <param name="windowHeight">The height of the window.</param>
/// <param name="scalingQuality">The scaling (filtering) method to use for the background canvas texture.</param>
public SdlWindow(string title, Point position, int windowWidth, int windowHeight, ScalingQuality scalingQuality) : this(title, position, windowWidth, windowHeight, windowWidth, windowHeight, scalingQuality) { }
/// <summary>
/// Creates a new <see cref="SdlWindow"/>.
/// </summary>
/// <param name="title">The text that is displayed on the window's title bar.</param>
/// <param name="position">A <see cref="Point"/> representing the starting position of the window. The X and Y coordinates of the Point can be set to <see cref="WindowPosUndefined"/> or <see cref="WindowPosCentered"/>.</param>
/// <param name="windowWidth">The width of the window.</param>
/// <param name="windowHeight">The height of the window.</param>
/// <param name="renderWidth">The width of the rendering target.</param>
/// <param name="renderHeight">The height of the rendering target.</param>
public SdlWindow(string title, Point position, int windowWidth, int windowHeight, int renderWidth, int renderHeight) : this(title, position, windowWidth, windowHeight, renderWidth, renderHeight, ScalingQuality.Nearest) { }
/// <summary>
/// Creates a new <see cref="SdlWindow"/>.
/// </summary>
/// <param name="title">The text that is displayed on the window's title bar.</param>
/// <param name="position">A <see cref="Point"/> representing the starting position of the window. The X and Y coordinates of the Point can be set to <see cref="WindowPosUndefined"/> or <see cref="WindowPosCentered"/>.</param>
/// <param name="windowWidth">The width of the window.</param>
/// <param name="windowHeight">The height of the window.</param>
/// <param name="renderWidth">The width of the rendering target.</param>
/// <param name="renderHeight">The height of the rendering target.</param>
/// <param name="scalingQuality">The scaling (filtering) method to use for the background canvas texture.</param>
public SdlWindow(string title, Point position, int windowWidth, int windowHeight, int renderWidth, int renderHeight, ScalingQuality scalingQuality) {
_sdlInit.InitSubsystem(Init.SubsystemFlags.Video);
_windowTitle = title;
_window = Video.CreateWindow(title, position.X, position.Y, windowWidth, windowHeight, Video.WindowFlags.Hidden);
_renderer = Render.CreateRenderer(_window, -1, Render.RendererFlags.Accelerated);
WindowWidth = windowWidth;
WindowHeight = windowHeight;
RenderWidth = renderWidth;
RenderHeight = renderHeight;
ScalingQuality = scalingQuality;
CreateTexture();
Layers = new List<Canvas> {
new Background(renderWidth, renderHeight) {
Renderer = _renderer,
BlendMode = BlendMode.None
}
};
Background.CreateTexture();
CameraView = new Rectangle(
new Point(0, 0),
new Point(WindowWidth, WindowHeight)
);
Sprites = new SpriteList(_renderer);
IsDestroyed = false;
_resources.RegisterResource(this);
}
/// <summary>
/// Releases resources used by the <see cref="SdlWindow"/> instance.
/// </summary>
~SdlWindow() {
DestroyObject();
_resources.UnregisterResource(this);
}
/// <summary>
/// Adds a background layer to the top of the layer stack.
/// </summary>
/// <param name="width">The width of the new layer's texture.</param>
/// <param name="height">The height of the new layer's texture.</param>
/// <param name="blendMode">The blending mode of the new layer.</param>
/// <returns>An integer identifying the new layer.</returns>
public int AddLayer(int width, int height, BlendMode blendMode = BlendMode.Alpha) {
var layer = new Background(width, height) {
Renderer = _renderer,
BlendMode = blendMode
};
layer.CreateTexture();
Layers.Add(layer);
return Layers.Count - 1;
}
/// <summary>
/// Handles calling the user draw function and passing the CLR objects to SDL2.
/// </summary>
private void BaseDraw() {
if(IsDestroyed || IsMinimized) return;
OnDraw();
Render.SetRenderTarget(_renderer, _texture);
// Blit the background canvases to the target texture.
foreach(var canvas in Layers) {
canvas.Clipping.Position = CameraView.Position;
canvas.Clipping.Size = CameraView.Size;
canvas.UpdateTexture();
unsafe {
var canvasClippingRect = canvas.Clipping.SdlRect;
Render.RenderCopy(_renderer, canvas.Texture, new IntPtr(&canvasClippingRect), IntPtr.Zero);
}
}
// Plot sprites on top of the background layer.
if(Sprites.Count > 0) DrawSprites();
Render.SetRenderTarget(_renderer, IntPtr.Zero);
Render.RenderCopy(_renderer, _texture, IntPtr.Zero, IntPtr.Zero);
Render.RenderPresent(_renderer);
}
/// <summary>
/// Handles setting up the <see cref="SdlWindow"/>.
/// </summary>
private void BaseLoad() {
OnLoad(); // Call the overridden Load function.
}
/// <summary>
/// Handles updating the application logic for the <see cref="SdlWindow"/>.
/// </summary>
private void BaseUpdate() {
if(IsDestroyed) return;
Events.EventHandler.ProcessEvents();
OnUpdate(); // Call the overridden Update function.
}
/// <summary>
/// Creates the rendering target that all of the layers will be drawn to prior to rendering.
/// </summary>
private void CreateTexture() {
DestroyTexture();
Hints.SetHint(Hints.RenderScaleQuality, ScalingQuality.ToString());
_texture = Render.CreateTexture(_renderer, Pixels.PixelFormatArgb8888, Render.TextureAccess.Target, RenderWidth, RenderHeight);
_hasTexture = true;
}
/// <summary>
/// Destroys this <see cref="SdlWindow"/>.
/// </summary>
public void DestroyObject() {
Video.DestroyWindow(_window);
IsDestroyed = true;
}
/// <summary>
/// Destroys the render target associated with this <see cref="SdlWindow"/>.
/// </summary>
private void DestroyTexture() {
if(!_hasTexture) return;
Render.DestroyTexture(_texture);
_hasTexture = false;
}
/// <summary>
/// Plots the sprites stored in <see cref="Sprites"/> to the screen. Please note that this method is called by
/// DotSDL's drawing routines and does not need to be called manually. Additionally, this method will not be
/// called if there are no sprites defined. You usually do not need to override this method.
/// </summary>
public virtual void DrawSprites() {
Render.SetRenderTarget(_renderer, _texture);
foreach(var sprite in Sprites.Where(e => e.Shown).OrderBy(e => e.ZOrder)) {
var srcRect = sprite.Clipping.SdlRect;
var drawSize = sprite.DrawSize;
Rectangle dest;
if(sprite.CoordinateSystem == CoordinateSystem.ScreenSpace) {
dest = new Rectangle(sprite.Position, drawSize);
} else {
// Create a set of world coordinates based on the position of the camera
// and this sprite.
var relPosition = new Point(sprite.Position - CameraView.Position);
var screenPosition = new Point(
(int)((float)relPosition.X / CameraView.Size.X * RenderWidth),
(int)((float)relPosition.Y / CameraView.Size.Y * RenderHeight)
);
var scaleFactorX = (float)RenderWidth / CameraView.Size.X;
var scaleFactorY = (float)RenderHeight / CameraView.Size.Y;
var size = new Point(
(int)(drawSize.X * scaleFactorX),
(int)(drawSize.Y * scaleFactorY)
);
dest = new Rectangle(screenPosition, size);
}
var destRect = dest.SdlRect;
unsafe {
var srcRectPtr = new IntPtr(&srcRect);
var destRectPtr = new IntPtr(&destRect);
Render.RenderCopyEx(
renderer: _renderer,
texture: sprite.Texture,
srcRect: srcRectPtr,
dstRect: destRectPtr,
angle: sprite.Rotation,
center: sprite.RotationCenter.SdlPoint,
flip: sprite.Flip
);
}
}
}
/// <summary>
/// Retrieves the SDL resource ID for this <see cref="SdlWindow"/>.
/// </summary>
/// <returns></returns>
public uint GetResourceId() {
return Video.GetWindowId(_window);
}
/// <summary>
/// Triggers this window to handle a specified <see cref="KeyboardEvent"/>.
/// </summary>
/// <param name="ev">The <see cref="KeyboardEvent"/> to handle.</param>
internal void HandleEvent(KeyboardEvent ev) {
switch(ev.State) {
case ButtonState.Pressed:
KeyPressed?.Invoke(this, ev);
break;
case ButtonState.Released:
KeyReleased?.Invoke(this, ev);
break;
}
}
/// <summary>
/// Triggers this window to handle a specified <see cref="WindowEvent"/>.
/// </summary>
/// <param name="ev">The <see cref="WindowEvent"/> to handle.</param>
internal void HandleEvent(WindowEvent ev) {
switch(ev.Event) {
case WindowEventType.Close:
OnClose(ev);
break;
case WindowEventType.Minimized:
OnMinimize(ev);
break;
case WindowEventType.Restored:
OnRestore(ev);
break;
}
}
/// <summary>
/// A game loop that calls the <see cref="SdlWindow"/> update and draw functions.
/// </summary>
private void Loop() {
_running = true;
while(_running) {
var ticks = TicksElapsed;
if(ticks > _nextGameUpdate || GameUpdateTicks == 0) {
_nextGameUpdate = ticks + GameUpdateTicks;
BaseUpdate();
}
if(ticks > _nextVideoUpdate || VideoUpdateTicks == 0) {
_nextVideoUpdate = ticks + VideoUpdateTicks;
BaseDraw();
}
if(VideoUpdateTicks <= 0 && GameUpdateTicks <= 0) continue; // Cook the CPU!
var updateTicks = (long)(_nextGameUpdate > _nextVideoUpdate ? _nextVideoUpdate : _nextGameUpdate) - TicksElapsed;
if(updateTicks > 0)
Timer.Delay((uint)updateTicks);
}
}
/// <summary>
/// Called when the window's close button is clicked.
/// </summary>
private void OnClose(WindowEvent ev) {
if(Closed is null) Stop();
else Closed(this, ev);
}
/// <summary>
/// Called every time the window is drawn to.
/// </summary>
protected virtual void OnDraw() { }
/// <summary>
/// Called before the window is shown.
/// </summary>
protected virtual void OnLoad() { }
/// <summary>
/// Called when the window is minimized.
/// </summary>
private void OnMinimize(WindowEvent ev) {
IsMinimized = true;
Minimized?.Invoke(this, ev);
}
/// <summary>
/// Called when the window is restored.
/// </summary>
private void OnRestore(WindowEvent ev) {
IsMinimized = false;
Restored?.Invoke(this, ev);
}
/// <summary>
/// Called every time the application logic update runs.
/// </summary>
protected virtual void OnUpdate() { }
/// <summary>
/// Removes a layer from the layer stack.
/// </summary>
/// <param name="id">The unique identifier of the layer to remove.</param>
/// <remarks>The background layer (layer 0) cannot be deleted.</remarks>
/// <exception cref="ArgumentOutOfRangeException"><c>id</c> is less than 0.</exception>
/// /// <exception cref="ArgumentOutOfRangeException"><c>id</c> is equal to or greater than <see cref="LayerCount"/>.</exception>
public void RemoveLayer(int id) {
if(id == 0) throw new ArgumentOutOfRangeException(nameof(id), "The background object (layer 0) cannot be deleted.");
Layers.RemoveAt(id);
}
/// <summary>
/// Displays the window and begins executing code that's associated with it.
/// </summary>
public void Start() {
Start(0, 0);
}
/// <summary>
/// Displays the window and begins executing code that's associated with it.
/// </summary>
/// <param name="updateRate">The desired number of milliseconds between frames and game logic updates. 0 causes the display and game to be continuously updated.</param>
public void Start(uint updateRate) {
Start(updateRate, updateRate);
}
/// <summary>
/// Displays the window and begins executing code that's associated with it.
/// </summary>
/// <param name="drawRate">The desired number of milliseconds between draw calls. 0 causes the display to be continuously updated.</param>
/// <param name="updateRate">The desired number of milliseconds between game logic updates. 0 causes the game to be continuously updated.</param>
public void Start(uint drawRate, uint updateRate) {
VideoUpdateTicks = drawRate;
GameUpdateTicks = updateRate;
BaseLoad();
Video.ShowWindow(_window);
Loop();
}
/// <summary>
/// Stops executing the game loop and destroys the window.
/// </summary>
public void Stop() {
_running = false;
DestroyObject();
}
}
}