using DotSDL.Events; using DotSDL.Input; using DotSDL.Interop.Core; using System; using System.Collections.Generic; using System.Linq; namespace DotSDL.Graphics { /// /// Represents an SDL window. /// 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; /// Gets the background layer of this window. This is equivalent to accessing Layers[0]. public Canvas Background => Layers[0]; /// Gets the list of background layers for this window. public List Layers { get; } /// /// Gets the number of background layers for this windows. This number will always be greater /// than or equal to 1. /// public int LayerCount => Layers.Count; /// true if this instance has been destroyed, othersize false. public bool IsDestroyed { get; set; } /// true if this has been minimized, othersize false. public bool IsMinimized { get; set; } public ResourceType ResourceType => ResourceType.Window; /// Gets the width of this . public int WindowWidth { get; } /// Gets the height of this . public int WindowHeight { get; } /// Gets or sets the title of this . public string WindowTitle { get => _windowTitle; set { _windowTitle = value; Video.SetWindowTitle(_window, _windowTitle); } } /// Gets the width of the rendering target used by this . public int RenderWidth { get; } /// Gets the height of the rendering target used by this . public int RenderHeight { get; } /// The amount of time, in milliseconds, from when the application was started. public uint TicksElapsed => Timer.GetTicks(); /// Gets or sets the amount of time, in milliseconds, between video updates. public uint VideoUpdateTicks { get; set; } /// Gets or sets the amount of time, in milliseconds, between game (logic) updates. public uint GameUpdateTicks { get; set; } /// Gets a that can be manipulated to modify how much of the scene is displayed. public Rectangle CameraView { get; } /// The list of active objects. public SpriteList Sprites { get; } /// Indicates that the window manager should position the window. To place the window on a specific display, use the function. public const int WindowPosUndefined = 0x1FFF0000; /// Fired when tboolhe window's close button is clicked. public event EventHandler Closed; /// Fired when a key is pressed. public event EventHandler KeyPressed; /// Fired when a key is released. public event EventHandler KeyReleased; /// Fired when the window's minimize button is clicked. public event EventHandler Minimized; /// Fired when the window is restored. public event EventHandler Restored; public ScalingQuality ScalingQuality { get => _scalingQuality; set { _scalingQuality = value; if(_hasTexture) CreateTexture(); } } /// /// Calculates a value that allows the window to be placed on a specific display, with its exact position determined by the window manager. /// /// The index of the display to place the window on. /// A coordinate value that should be passed to the constructor. public static int WindowPosUndefinedDisplay(uint display) { return (int)(WindowPosUndefined | display); } /// /// Indicates that the window should be in the center of the screen. To center the window on a specific display, use the function. /// public const int WindowPosCentered = 0x2FFF0000; /// /// Calculates a value that allows the window to be placed in the center of a specified display. /// /// The index of the display to place the window on. /// A coordinate value that should be passed to the constructor. public static int WindowPosCenteredDisplay(uint display) { return (int)(WindowPosCentered | display); } /// /// Creates a new .readonly /// /// The text that is displayed on the window's title bar. /// A representing the starting position of the window. The X and Y coordinates of the Point can be set to or . /// The width of the window. /// The height of the window. public SdlWindow(string title, Point position, int windowWidth, int windowHeight) : this(title, position, windowWidth, windowHeight, windowWidth, windowHeight, ScalingQuality.Nearest) { } /// /// Creates a new . /// /// The text that is displayed on the window's title bar. /// A representing the starting position of the window. The X and Y coordinates of the Point can be set to or . /// The width of the window. /// The height of the window. /// The scaling (filtering) method to use for the background canvas texture. public SdlWindow(string title, Point position, int windowWidth, int windowHeight, ScalingQuality scalingQuality) : this(title, position, windowWidth, windowHeight, windowWidth, windowHeight, scalingQuality) { } /// /// Creates a new . /// /// The text that is displayed on the window's title bar. /// A representing the starting position of the window. The X and Y coordinates of the Point can be set to or . /// The width of the window. /// The height of the window. /// The width of the rendering target. /// The height of the rendering target. public SdlWindow(string title, Point position, int windowWidth, int windowHeight, int renderWidth, int renderHeight) : this(title, position, windowWidth, windowHeight, renderWidth, renderHeight, ScalingQuality.Nearest) { } /// /// Creates a new . /// /// The text that is displayed on the window's title bar. /// A representing the starting position of the window. The X and Y coordinates of the Point can be set to or . /// The width of the window. /// The height of the window. /// The width of the rendering target. /// The height of the rendering target. /// The scaling (filtering) method to use for the background canvas texture. 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 { 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); } /// /// Releases resources used by the instance. /// ~SdlWindow() { DestroyObject(); _resources.UnregisterResource(this); } /// /// Adds a background layer to the top of the layer stack. /// /// The width of the new layer's texture. /// The height of the new layer's texture. /// The blending mode of the new layer. /// An integer identifying the new layer. 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; } /// /// Handles calling the user draw function and passing the CLR objects to SDL2. /// 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); } /// /// Handles setting up the . /// private void BaseLoad() { OnLoad(); // Call the overridden Load function. } /// /// Handles updating the application logic for the . /// private void BaseUpdate() { if(IsDestroyed) return; Events.EventHandler.ProcessEvents(); OnUpdate(); // Call the overridden Update function. } /// /// Creates the rendering target that all of the layers will be drawn to prior to rendering. /// private void CreateTexture() { DestroyTexture(); Hints.SetHint(Hints.RenderScaleQuality, ScalingQuality.ToString()); _texture = Render.CreateTexture(_renderer, Pixels.PixelFormatArgb8888, Render.TextureAccess.Target, RenderWidth, RenderHeight); _hasTexture = true; } /// /// Destroys this . /// public void DestroyObject() { Video.DestroyWindow(_window); IsDestroyed = true; } /// /// Destroys the render target associated with this . /// private void DestroyTexture() { if(!_hasTexture) return; Render.DestroyTexture(_texture); _hasTexture = false; } /// /// Plots the sprites stored in 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. /// 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 ); } } } /// /// Retrieves the SDL resource ID for this . /// /// public uint GetResourceId() { return Video.GetWindowId(_window); } /// /// Triggers this window to handle a specified . /// /// The to handle. internal void HandleEvent(KeyboardEvent ev) { switch(ev.State) { case ButtonState.Pressed: KeyPressed?.Invoke(this, ev); break; case ButtonState.Released: KeyReleased?.Invoke(this, ev); break; } } /// /// Triggers this window to handle a specified . /// /// The to handle. 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; } } /// /// A game loop that calls the update and draw functions. /// 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); } } /// /// Called when the window's close button is clicked. /// private void OnClose(WindowEvent ev) { if(Closed is null) Stop(); else Closed(this, ev); } /// /// Called every time the window is drawn to. /// protected virtual void OnDraw() { } /// /// Called before the window is shown. /// protected virtual void OnLoad() { } /// /// Called when the window is minimized. /// private void OnMinimize(WindowEvent ev) { IsMinimized = true; Minimized?.Invoke(this, ev); } /// /// Called when the window is restored. /// private void OnRestore(WindowEvent ev) { IsMinimized = false; Restored?.Invoke(this, ev); } /// /// Called every time the application logic update runs. /// protected virtual void OnUpdate() { } /// /// Removes a layer from the layer stack. /// /// The unique identifier of the layer to remove. /// The background layer (layer 0) cannot be deleted. /// id is less than 0. /// /// id is equal to or greater than . public void RemoveLayer(int id) { if(id == 0) throw new ArgumentOutOfRangeException(nameof(id), "The background object (layer 0) cannot be deleted."); Layers.RemoveAt(id); } /// /// Displays the window and begins executing code that's associated with it. /// public void Start() { Start(0, 0); } /// /// Displays the window and begins executing code that's associated with it. /// /// The desired number of milliseconds between frames and game logic updates. 0 causes the display and game to be continuously updated. public void Start(uint updateRate) { Start(updateRate, updateRate); } /// /// Displays the window and begins executing code that's associated with it. /// /// The desired number of milliseconds between draw calls. 0 causes the display to be continuously updated. /// The desired number of milliseconds between game logic updates. 0 causes the game to be continuously updated. public void Start(uint drawRate, uint updateRate) { VideoUpdateTicks = drawRate; GameUpdateTicks = updateRate; BaseLoad(); Video.ShowWindow(_window); Loop(); } /// /// Stops executing the game loop and destroys the window. /// public void Stop() { _running = false; DestroyObject(); } } }