diff --git a/DotSDL/DotSDL.csproj b/DotSDL/DotSDL.csproj index 31f203e..fe9d32b 100644 --- a/DotSDL/DotSDL.csproj +++ b/DotSDL/DotSDL.csproj @@ -5,10 +5,13 @@ 0.0.1.0 0.0.1.0 A framework for .NET designed to interoperate with SDL2. - (c) 2017 Ian Burgmyer + (c) 2019 Ian Burgmyer Ian Burgmyer The DotSDL Team 7.2 + DotSDL + https://github.com/Spectere/DotSDL + https://github.com/Spectere/DotSDL/blob/master/LICENSE.txt True @@ -16,4 +19,7 @@ True + + + \ No newline at end of file diff --git a/DotSDL/Graphics/CoordinateSystem.cs b/DotSDL/Graphics/CoordinateSystem.cs new file mode 100644 index 0000000..e061a61 --- /dev/null +++ b/DotSDL/Graphics/CoordinateSystem.cs @@ -0,0 +1,14 @@ +namespace DotSDL.Graphics { + public enum CoordinateSystem { + /// + /// Uses screen space coordinates. This directly maps to pixels in the window. + /// + ScreenSpace, + + /// + /// Uses world space coordinates. This maps to a location based on the position of + /// the camera, or view. + /// + WorldSpace + } +} diff --git a/DotSDL/Graphics/SdlWindow.cs b/DotSDL/Graphics/SdlWindow.cs index cf5784b..5b39347 100644 --- a/DotSDL/Graphics/SdlWindow.cs +++ b/DotSDL/Graphics/SdlWindow.cs @@ -61,6 +61,9 @@ namespace DotSDL.Graphics { /// 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; } @@ -177,6 +180,11 @@ namespace DotSDL.Graphics { }; Background.CreateTexture(); + CameraView = new Rectangle( + new Point(0, 0), + new Point(WindowWidth, WindowHeight) + ); + Sprites = new SpriteList(_renderer); IsDestroyed = false; @@ -202,6 +210,8 @@ namespace DotSDL.Graphics { Render.SetRenderTarget(_renderer, _texture); // Blit the Canvas to the target texture. + Background.Clipping.Position = CameraView.Position; + Background.Clipping.Size = CameraView.Size; Background.UpdateTexture(); unsafe { var canvasClippingRect = Background.Clipping.SdlRect; @@ -271,11 +281,30 @@ namespace DotSDL.Graphics { Render.SetRenderTarget(_renderer, _texture); foreach(var sprite in Sprites.Where(e => e.Shown).OrderBy(e => e.ZOrder)) { var srcRect = sprite.Clipping.SdlRect; - var drawSize = new Point( - (int)(srcRect.W * sprite.Scale.X), - (int)(srcRect.H * sprite.Scale.Y) - ); - var destRect = new Rectangle(sprite.Position, drawSize).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 * TextureWidth), + (int)((float)relPosition.Y / CameraView.Size.Y * TextureHeight) + ); + var scaleFactorX = (float)TextureWidth / CameraView.Size.X; + var scaleFactorY = (float)TextureHeight / 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); diff --git a/DotSDL/Graphics/Sprite.cs b/DotSDL/Graphics/Sprite.cs index 93c162f..a787122 100644 --- a/DotSDL/Graphics/Sprite.cs +++ b/DotSDL/Graphics/Sprite.cs @@ -7,6 +7,9 @@ namespace DotSDL.Graphics { /// Represents a graphical, two-dimensional object in a program. /// public class Sprite : Canvas { + private Point _effectiveSize = new Point(); + private Vector2 _scale; + /// /// The position on the screen where the should be drawn. /// @@ -32,7 +35,14 @@ namespace DotSDL.Graphics { /// /// The scale of the . 1.0f is 100%. /// - public Vector2 Scale { get; set; } + public Vector2 Scale { + get => _scale; + set { + _scale = value; + _effectiveSize.X = (int)(Width * _scale.X); + _effectiveSize.Y = (int)(Height * _scale.Y); + } + } /// public override ScalingQuality ScalingQuality { @@ -47,6 +57,32 @@ namespace DotSDL.Graphics { } } + /// + /// Gets the size of the sprite as it would be drawn to the screen, in pixels, taking + /// both scaling and clipping into account. + /// + /// + /// This value is calculated every time it's called. If you plan to the results of this + /// in a loop, be sure to save the results to a variable and perform operations on that + /// instead of calling this multiple times. + /// + public Point DrawSize => new Point( + (int)(Clipping.Size.X * Scale.X), + (int)(Clipping.Size.Y * Scale.Y) + ); + + /// + /// Gets the effective size of the sprite. This is the size of the sprite, in pixels, + /// after taking scaling into account. + /// + public Point EffectiveSize => _effectiveSize; + + /// + /// Defines the coordinate system that this sprite should use. This defaults to + /// . + /// + public CoordinateSystem CoordinateSystem { get; set; } = CoordinateSystem.WorldSpace; + /// /// true if the sprite should be drawn to the screen, otherwise false. /// diff --git a/DotSDL/Math/Vector/Vector2.cs b/DotSDL/Math/Vector/Vector2.cs index d16779b..441e99e 100644 --- a/DotSDL/Math/Vector/Vector2.cs +++ b/DotSDL/Math/Vector/Vector2.cs @@ -73,6 +73,57 @@ namespace DotSDL.Math.Vector { return !(left == right); } + /// + /// Adds two objects together. + /// + /// The to use as the operator. + /// The to use as the operand. + /// A containing the result. + public static Vector2 operator +(Vector2 left, Vector2 right) => + new Vector2( + (dynamic)left.X + (dynamic)right.X, + (dynamic)left.Y + (dynamic)right.Y + ); + + /// + /// Adds a object with a scalar value. + /// + /// The to use as the operator. + /// The to use as the operand. + /// A containing the result. + public static Vector2 operator +(Vector2 vec, T scalar) => + new Vector2((dynamic)vec.X + scalar, (dynamic)vec.Y + scalar); + + /// + /// Subtracts two objects. + /// + /// The to use as the operator. + /// The to use as the operand. + /// A containing the result. + public static Vector2 operator -(Vector2 left, Vector2 right) => + new Vector2( + (dynamic)left.X - (dynamic)right.X, + (dynamic)left.Y - (dynamic)right.Y + ); + + /// + /// Multiples a by a scalar value. + /// + /// The to use as the operator. + /// The to use as the operand. + /// A containing the result. + public static Vector2 operator *(Vector2 vec, T scalar) => + new Vector2((dynamic)vec.X * scalar, (dynamic)vec.Y + scalar); + + /// + /// Subtracts a scalar value from a . + /// + /// The to use as the operator. + /// The to use as the operand. + /// A containing the result. + public static Vector2 operator -(Vector2 vec, T scalar) => + new Vector2((dynamic)vec.X - scalar, (dynamic)vec.Y - scalar); + /// Returns a new with both X and Y set to 1. public static Vector2 One => new Vector2(VectorBase.GetOne()); @@ -85,6 +136,15 @@ namespace DotSDL.Math.Vector { /// Returns a new with both X and Y set to 0. public static Vector2 Zero => new Vector2(VectorBase.GetZero()); + /// + /// Calculates the dot product of two objects. + /// + /// The to use as the operator. + /// The to use as the operand. + /// A containing the result. + public T Dot(Vector2 left, Vector2 right) => + (dynamic)left.X * (dynamic)right.X + (dynamic)left.Y + (dynamic)right.Y; + /// public bool Equals(Vector2 other) { if(ReferenceEquals(null, other)) return false; @@ -108,5 +168,7 @@ namespace DotSDL.Math.Vector { return (EqualityComparer.Default.GetHashCode(X) * 397) ^ EqualityComparer.Default.GetHashCode(Y); } } + + public override string ToString() => $"{X}, {Y}"; } } diff --git a/README.md b/README.md index 4b3ccf3..fe081e4 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,9 @@ At this time, DotSDL supports the following features: * Keyboard input. * Window events. * Graphics - * A single 32-bit ARGB canvas (useful for pixel plotting). + * A single 32-bit ARGB background canvas (useful for pixel plotting). + * Sprite drawing, with support for alpha blending, world and screen space + rendering, and scaling. * Power * Battery state. diff --git a/Samples/Sample.Sprites/Player.cs b/Samples/Sample.Sprites/Player.cs index c71f323..74fde37 100644 --- a/Samples/Sample.Sprites/Player.cs +++ b/Samples/Sample.Sprites/Player.cs @@ -1,17 +1,20 @@ -using DotSDL.Graphics; +using System; +using DotSDL.Graphics; namespace Sample.Sprites { public class Player : Sprite { private const int Radius = 15; private const int Size = 32; - private int _speed; + private readonly int _speed; + private readonly Point _limit; - public Player(Color color, int speed, int playerId) : base(Size, Size, playerId) { + public Player(Color color, int speed, int playerId, Point limit) : base(Size, Size, playerId) { const byte baseAlpha = 128; ColorMod = color; _speed = speed; + _limit = limit; Shown = true; @@ -51,9 +54,11 @@ namespace Sample.Sprites { } public void Move(Point delta) { - // TODO: Support some basic vector arithmetic for points. Position.X += delta.X * _speed; Position.Y += delta.Y * _speed; + + Position.X = Math.Clamp(Position.X, 0, (int)(_limit.X - (Size * Scale.X))); + Position.Y = Math.Clamp(Position.Y, 0, (int)(_limit.Y - (Size * Scale.Y))); } private void PlotMirroredPoints(int x, int y, byte gray, byte alpha) { diff --git a/Samples/Sample.Sprites/Window.cs b/Samples/Sample.Sprites/Window.cs index 53cca15..81529b0 100644 --- a/Samples/Sample.Sprites/Window.cs +++ b/Samples/Sample.Sprites/Window.cs @@ -5,10 +5,11 @@ using System; namespace Sample.Sprites { public class Window : SdlWindow { - //private int _camX = 0, _camY = 0, _deltaX = 2, _deltaY = 1; private Player _player1, _player2; private Point _player1Delta, _player2Delta; + private const int ViewMargin = 16; + public Window(int scale) : base("Sprites Test", new Point(WindowPosUndefined, WindowPosUndefined), 256 * scale, 196 * scale, @@ -76,8 +77,9 @@ namespace Sample.Sprites { } private void GeneratePlayers() { - _player1 = new Player(new Color { R = 255, G = 64, B = 64 }, 2, 1); - _player2 = new Player(new Color { R = 64, G = 64, B = 255 }, 3, 2); + var limit = new Point(1024, 1024); + _player1 = new Player(new Color { R = 255, G = 64, B = 64 }, 2, 1, limit); + _player2 = new Player(new Color { R = 64, G = 64, B = 255 }, 3, 2, limit); _player1.Position.X = 24; _player1.Position.Y = 24; @@ -157,41 +159,33 @@ namespace Sample.Sprites { _player1.Move(_player1Delta); _player2.Move(_player2Delta); - var x1 = _player1.Position.X <= _player2.Position.X - ? _player1.Position.X - (_player1.Width * _player1.Scale.X) - : _player2.Position.X - (_player2.Width * _player2.Scale.X); - var x2 = _player1.Position.X >= _player2.Position.X - ? _player1.Position.X + (_player1.Width * _player1.Scale.X) - : _player2.Position.X + (_player2.Width * _player2.Scale.X); - var y1 = _player1.Position.Y <= _player2.Position.Y - ? _player1.Position.Y - (_player1.Height * _player1.Scale.Y) - : _player2.Position.Y - (_player2.Height * _player2.Scale.Y); - var y2 = _player1.Position.Y >= _player2.Position.Y - ? _player1.Position.Y + (_player1.Height * _player1.Scale.Y) - : _player2.Position.Y + (_player2.Height * _player2.Scale.Y); - - x1 = x1 < 0 ? 0 : x1; - x2 = x2 >= Background.Width ? Background.Width : x2; - y1 = y1 < 0 ? 0 : y1; - y2 = y2 >= Background.Height ? Background.Height : y2; - - Background.Clipping.Position.X = (int)x1; - Background.Clipping.Position.Y = (int)y1; - Background.Clipping.Size.X = (int)(x2 - x1); - Background.Clipping.Size.Y = (int)(y2 - y1); - - WindowTitle = $"({(int)x1} {(int)y2}), ({(int)(x2 - x1)}, {(int)(y2 - y1)}) / ({Background.Width}, {Background.Height})"; - - /*_camX += _deltaX; - _camY += _deltaY; - - if(_camX + Background.Clipping.Size.X >= Background.Width || _camX <= 0) - _deltaX = -_deltaX; - if(_camY + Background.Clipping.Size.Y >= Background.Height || _camY <= 0) - _deltaY = -_deltaY; - - Background.Clipping.Position.X = _camX; - Background.Clipping.Position.Y = _camY;*/ + var p1End = _player1.Position + _player1.DrawSize; + var p2End = _player2.Position + _player2.DrawSize; + + var x1 = (_player1.Position.X <= _player2.Position.X + ? _player1.Position.X + : _player2.Position.X) - ViewMargin; + + var x2 = (p1End.X >= p2End.X ? p1End.X : p2End.X) + ViewMargin; + + var y1 = (_player1.Position.Y <= _player2.Position.Y + ? _player1.Position.Y + : _player2.Position.Y) - ViewMargin; + + var y2 = (p1End.Y >= p2End.Y ? p1End.Y : p2End.Y) + ViewMargin; + + x1 = Math.Clamp(x1, 0, Background.Width - ViewMargin); + x2 = Math.Clamp(x2, ViewMargin, Background.Width); + y1 = Math.Clamp(y1, 0, Background.Height - ViewMargin); + y2 = Math.Clamp(y2, ViewMargin, Background.Height); + + CameraView.Position.X = x1; + CameraView.Position.Y = y1; + CameraView.Size.X = x2 - x1; + CameraView.Size.Y = y2 - y1; + + WindowTitle = $"({x1} {y2}), ({x2 - x1}, {y2 - y1})" + + $" | P1: {_player1.Position} | P2: {_player2.Position}"; } } }