1
0
Fork 0
mirror of https://github.com/Ellpeck/MLEM.git synced 2024-05-20 08:01:21 +02:00

finished xml docs for the MLEM core package

This commit is contained in:
Ellpeck 2020-05-21 17:21:34 +02:00
parent e9cc9b7d99
commit dc48c4caa1
21 changed files with 888 additions and 5 deletions

View file

@ -138,6 +138,10 @@ namespace MLEM.Animations {
this.IsPaused = false;
}
/// <summary>
/// A callback for when a sprite animation is completed.
/// </summary>
/// <param name="animation">The animation that has completed</param>
public delegate void Completed(SpriteAnimation animation);
}

View file

@ -98,6 +98,11 @@ namespace MLEM.Animations {
}
}
/// <summary>
/// A callback delegate for when a <see cref="SpriteAnimationGroup"/>'s current animation changed.
/// </summary>
/// <param name="oldAnim">The previous animation</param>
/// <param name="newAnim">The new animation</param>
public delegate void AnimationChanged(SpriteAnimation oldAnim, SpriteAnimation newAnim);
}

View file

@ -40,11 +40,11 @@ namespace MLEM.Extensions {
}
/// <summary>
/// Copies the alpha value from <see cref="other"/> into this color.
/// Copies the alpha value from another color into this color.
/// </summary>
/// <param name="color">The color</param>
/// <param name="other">The color to copy the alpha from</param>
/// <returns>The <see cref="color"/> with <see cref="other"/>'s alpha value</returns>
/// <returns>The first color with the second color's alpha value</returns>
public static Color CopyAlpha(this Color color, Color other) {
return color * (other.A / 255F);
}

View file

@ -75,17 +75,29 @@ namespace MLEM.Extensions {
return new TargetContext(device, target);
}
/// <summary>
/// Represents a context in which a <see cref="RenderTarget2D"/> is applied.
/// This class should be used with <see cref="GraphicsExtensions.WithRenderTarget"/>.
/// </summary>
public struct TargetContext : IDisposable {
private readonly GraphicsDevice device;
private readonly RenderTargetBinding[] lastTargets;
/// <summary>
/// Creates a new target context with the given settings.
/// </summary>
/// <param name="device">The graphics device to apply the target on</param>
/// <param name="target">The target to apply</param>
public TargetContext(GraphicsDevice device, RenderTarget2D target) {
this.device = device;
this.lastTargets = device.RenderTargetCount <= 0 ? null : device.GetRenderTargets();
device.SetRenderTarget(target);
}
/// <summary>
/// Disposes this target context, which causes the graphics device's previous render targets to be re-applied.
/// </summary>
public void Dispose() {
this.device.SetRenderTargets(this.lastTargets);
}

View file

@ -24,7 +24,7 @@ namespace MLEM.Extensions {
/// <param name="first">The first number to equate</param>
/// <param name="second">The second number to equate</param>
/// <param name="tolerance">The equality tolerance</param>
/// <returns>Whether or not <see cref="first"/> and <see cref="second"/> are different by at most <see cref="tolerance"/></returns>
/// <returns>Whether or not the two values are different by at most <c>tolerance</c></returns>
public static bool Equals(this float first, float second, float tolerance) {
return Math.Abs(first - second) <= tolerance;
}

View file

@ -9,47 +9,125 @@ using MLEM.Extensions;
using MLEM.Misc;
namespace MLEM.Input {
/// <summary>
/// An input handler is a more advanced wrapper around MonoGame's default input system.
/// It includes keyboard, mouse, gamepad and touch states, as well as a new "pressed" state for keys and the ability for keyboard and gamepad repeat events.
/// </summary>
public class InputHandler : GameComponent {
/// <summary>
/// Contains the keyboard state from the last update call
/// </summary>
public KeyboardState LastKeyboardState { get; private set; }
/// <summary>
/// Contains the current keyboard state
/// </summary>
public KeyboardState KeyboardState { get; private set; }
/// <summary>
/// Contains the keyboard keys that are currently being pressed
/// </summary>
public Keys[] PressedKeys { get; private set; }
/// <summary>
/// Set this property to false to disable keyboard handling for this input handler.
/// </summary>
public bool HandleKeyboard;
/// <summary>
/// Contains the mouse state from the last update call
/// </summary>
public MouseState LastMouseState { get; private set; }
/// <summary>
/// Contains the current mouse state
/// </summary>
public MouseState MouseState { get; private set; }
/// <summary>
/// Contains the current position of the mouse, extracted from <see cref="MouseState"/>
/// </summary>
public Point MousePosition => this.MouseState.Position;
/// <summary>
/// Contains the position of the mouse from the last update call, extracted from <see cref="LastMouseState"/>
/// </summary>
public Point LastMousePosition => this.LastMouseState.Position;
/// <summary>
/// Contains the current scroll wheel value, in increments of 120
/// </summary>
public int ScrollWheel => this.MouseState.ScrollWheelValue;
/// <summary>
/// Contains the scroll wheel value from the last update call, in increments of 120
/// </summary>
public int LastScrollWheel => this.LastMouseState.ScrollWheelValue;
/// <summary>
/// Set this property to false to disable mouse handling for this input handler.
/// </summary>
public bool HandleMouse;
private readonly GamePadState[] lastGamepads = new GamePadState[GamePad.MaximumGamePadCount];
private readonly GamePadState[] gamepads = new GamePadState[GamePad.MaximumGamePadCount];
/// <summary>
/// Contains the amount of gamepads that are currently connected.
/// This property is automatically updated in <see cref="Update()"/>
/// </summary>
public int ConnectedGamepads { get; private set; }
/// <summary>
/// Set this property to false to disable keyboard handling for this input handler.
/// </summary>
public bool HandleGamepads;
/// <summary>
/// Contains the touch state from the last update call
/// </summary>
public TouchCollection LastTouchState { get; private set; }
/// <summary>
/// Contains the current touch state
/// </summary>
public TouchCollection TouchState { get; private set; }
/// <summary>
/// Contains all of the gestures that have finished during the last update call.
/// To easily query these gestures, use <see cref="GetGesture"/>
/// </summary>
public readonly ReadOnlyCollection<GestureSample> Gestures;
private readonly List<GestureSample> gestures = new List<GestureSample>();
/// <summary>
/// Set this property to false to disable touch handling for this input handler.
/// </summary>
public bool HandleTouch;
/// <summary>
/// This is the amount of time that has to pass before the first keyboard repeat event is triggered.
/// <seealso cref="KeyRepeatRate"/>
/// </summary>
public TimeSpan KeyRepeatDelay = TimeSpan.FromSeconds(0.65);
/// <summary>
/// This is the amount of time that has to pass between keyboard repeat events.
/// <seealso cref="KeyRepeatDelay"/>
/// </summary>
public TimeSpan KeyRepeatRate = TimeSpan.FromSeconds(0.05);
/// <summary>
/// Set this property to false to disable keyboard repeat event handling.
/// </summary>
public bool HandleKeyboardRepeats = true;
private DateTime heldKeyStart;
private DateTime lastKeyRepeat;
private bool triggerKeyRepeat;
private Keys heldKey;
/// <summary>
/// Set this property to false to disable gamepad repeat event handling.
/// </summary>
public bool HandleGamepadRepeats = true;
private readonly DateTime[] heldGamepadButtonStarts = new DateTime[GamePad.MaximumGamePadCount];
private readonly DateTime[] lastGamepadButtonRepeats = new DateTime[GamePad.MaximumGamePadCount];
private readonly bool[] triggerGamepadButtonRepeat = new bool[GamePad.MaximumGamePadCount];
private readonly Buttons?[] heldGamepadButtons = new Buttons?[GamePad.MaximumGamePadCount];
/// <summary>
/// Creates a new input handler with optional initial values.
/// </summary>
/// <param name="handleKeyboard">If keyboard input should be handled</param>
/// <param name="handleMouse">If mouse input should be handled</param>
/// <param name="handleGamepads">If gamepad input should be handled</param>
/// <param name="handleTouch">If touch input should be handled</param>
public InputHandler(bool handleKeyboard = true, bool handleMouse = true, bool handleGamepads = true, bool handleTouch = true) : base(null) {
this.HandleKeyboard = handleKeyboard;
this.HandleMouse = handleMouse;
@ -58,6 +136,10 @@ namespace MLEM.Input {
this.Gestures = this.gestures.AsReadOnly();
}
/// <summary>
/// Updates this input handler, querying pressed and released keys and calculating repeat events.
/// Call this in your <see cref="Game.Update"/> method.
/// </summary>
public void Update() {
if (this.HandleKeyboard) {
this.LastKeyboardState = this.KeyboardState;
@ -151,34 +233,55 @@ namespace MLEM.Input {
}
}
/// <inheritdoc cref="Update()"/>
public override void Update(GameTime gameTime) {
this.Update();
}
/// <summary>
/// Returns the state of the <c>index</c>th gamepad from the last update call
/// </summary>
/// <param name="index">The zero-based gamepad index</param>
/// <returns>The state of the gamepad last update</returns>
public GamePadState GetLastGamepadState(int index) {
return this.lastGamepads[index];
}
/// <summary>
/// Returns the current state of the <c>index</c>th gamepad
/// </summary>
/// <param name="index">The zero-based gamepad index</param>
/// <returns>The current state of the gamepad</returns>
public GamePadState GetGamepadState(int index) {
return this.gamepads[index];
}
/// <inheritdoc cref="Microsoft.Xna.Framework.Input.KeyboardState.IsKeyDown"/>
public bool IsKeyDown(Keys key) {
return this.KeyboardState.IsKeyDown(key);
}
/// <inheritdoc cref="Microsoft.Xna.Framework.Input.KeyboardState.IsKeyUp"/>
public bool IsKeyUp(Keys key) {
return this.KeyboardState.IsKeyUp(key);
}
/// <inheritdoc cref="Microsoft.Xna.Framework.Input.KeyboardState.IsKeyDown"/>
public bool WasKeyDown(Keys key) {
return this.LastKeyboardState.IsKeyDown(key);
}
/// <inheritdoc cref="Microsoft.Xna.Framework.Input.KeyboardState.IsKeyUp"/>
public bool WasKeyUp(Keys key) {
return this.LastKeyboardState.IsKeyUp(key);
}
/// <summary>
/// Returns whether the given key is considered pressed.
/// A key is considered pressed if it was not down the last update call, but is down the current update call.
/// </summary>
/// <param name="key">The key to query</param>
/// <returns>If the key is pressed</returns>
public bool IsKeyPressed(Keys key) {
// if the queried key is the held key and a repeat should be triggered, return true
if (this.HandleKeyboardRepeats && key == this.heldKey && this.triggerKeyRepeat)
@ -186,30 +289,62 @@ namespace MLEM.Input {
return this.WasKeyUp(key) && this.IsKeyDown(key);
}
/// <summary>
/// Returns whether the given modifier key is down.
/// </summary>
/// <param name="modifier">The modifier key</param>
/// <returns>If the modifier key is down</returns>
public bool IsModifierKeyDown(ModifierKey modifier) {
return modifier.GetKeys().Any(this.IsKeyDown);
}
/// <summary>
/// Returns whether the given mouse button is currently down.
/// </summary>
/// <param name="button">The button to query</param>
/// <returns>Whether or not the queried button is down</returns>
public bool IsMouseButtonDown(MouseButton button) {
return this.MouseState.GetState(button) == ButtonState.Pressed;
}
/// <summary>
/// Returns whether the given mouse button is currently up.
/// </summary>
/// <param name="button">The button to query</param>
/// <returns>Whether or not the queried button is up</returns>
public bool IsMouseButtonUp(MouseButton button) {
return this.MouseState.GetState(button) == ButtonState.Released;
}
/// <summary>
/// Returns whether the given mouse button was down the last update call.
/// </summary>
/// <param name="button">The button to query</param>
/// <returns>Whether or not the queried button was down</returns>
public bool WasMouseButtonDown(MouseButton button) {
return this.LastMouseState.GetState(button) == ButtonState.Pressed;
}
/// <summary>
/// Returns whether the given mouse button was up the last update call.
/// </summary>
/// <param name="button">The button to query</param>
/// <returns>Whether or not the queried button was up</returns>
public bool WasMouseButtonUp(MouseButton button) {
return this.LastMouseState.GetState(button) == ButtonState.Released;
}
/// <summary>
/// Returns whether the given mouse button is considered pressed.
/// A mouse button is considered pressed if it was up the last update call, and is down the current update call.
/// </summary>
/// <param name="button">The button to query</param>
/// <returns>Whether the button is pressed</returns>
public bool IsMouseButtonPressed(MouseButton button) {
return this.WasMouseButtonUp(button) && this.IsMouseButtonDown(button);
}
/// <inheritdoc cref="GamePadState.IsButtonDown"/>
public bool IsGamepadButtonDown(Buttons button, int index = -1) {
if (index < 0) {
for (var i = 0; i < this.ConnectedGamepads; i++)
@ -220,6 +355,7 @@ namespace MLEM.Input {
return this.GetGamepadState(index).IsButtonDown(button);
}
/// <inheritdoc cref="GamePadState.IsButtonUp"/>
public bool IsGamepadButtonUp(Buttons button, int index = -1) {
if (index < 0) {
for (var i = 0; i < this.ConnectedGamepads; i++)
@ -230,6 +366,7 @@ namespace MLEM.Input {
return this.GetGamepadState(index).IsButtonUp(button);
}
/// <inheritdoc cref="GamePadState.IsButtonDown"/>
public bool WasGamepadButtonDown(Buttons button, int index = -1) {
if (index < 0) {
for (var i = 0; i < this.ConnectedGamepads; i++)
@ -240,6 +377,7 @@ namespace MLEM.Input {
return this.GetLastGamepadState(index).IsButtonDown(button);
}
/// <inheritdoc cref="GamePadState.IsButtonUp"/>
public bool WasGamepadButtonUp(Buttons button, int index = -1) {
if (index < 0) {
for (var i = 0; i < this.ConnectedGamepads; i++)
@ -250,6 +388,13 @@ namespace MLEM.Input {
return this.GetLastGamepadState(index).IsButtonUp(button);
}
/// <summary>
/// Returns whether the given gamepad button on the given index is considered pressed.
/// A gamepad button is considered pressed if it was down the last update call, and is up the current update call.
/// </summary>
/// <param name="button">The button to query</param>
/// <param name="index">The zero-based index of the gamepad, or -1 for any gamepad</param>
/// <returns>Whether the given button is pressed</returns>
public bool IsGamepadButtonPressed(Buttons button, int index = -1) {
if (this.HandleGamepadRepeats) {
if (index < 0) {
@ -263,6 +408,12 @@ namespace MLEM.Input {
return this.WasGamepadButtonUp(button, index) && this.IsGamepadButtonDown(button, index);
}
/// <summary>
/// Queries for a gesture of a given type that finished during the current update call.
/// </summary>
/// <param name="type">The type of gesture to query for</param>
/// <param name="sample">The resulting gesture sample, or default if there isn't one</param>
/// <returns>True if a gesture of the type was found, otherwise false</returns>
public bool GetGesture(GestureType type, out GestureSample sample) {
foreach (var gesture in this.Gestures) {
if (gesture.GestureType == type) {
@ -273,6 +424,14 @@ namespace MLEM.Input {
return false;
}
/// <summary>
/// Returns if a given control of any kind is down.
/// This is a helper function that can be passed a <see cref="Keys"/>, <see cref="Buttons"/> or <see cref="MouseButton"/>.
/// </summary>
/// <param name="control">The control whose down state to query</param>
/// <param name="index">The index of the gamepad to query (if applicable), or -1 for any gamepad</param>
/// <returns>Whether the given control is down</returns>
/// <exception cref="ArgumentException">If the passed control isn't of a supported type</exception>
public bool IsDown(object control, int index = -1) {
if (control is Keys key)
return this.IsKeyDown(key);
@ -283,6 +442,14 @@ namespace MLEM.Input {
throw new ArgumentException(nameof(control));
}
/// <summary>
/// Returns if a given control of any kind is up.
/// This is a helper function that can be passed a <see cref="Keys"/>, <see cref="Buttons"/> or <see cref="MouseButton"/>.
/// </summary>
/// <param name="control">The control whose up state to query</param>
/// <param name="index">The index of the gamepad to query (if applicable), or -1 for any gamepad</param>
/// <returns>Whether the given control is down</returns>
/// <exception cref="ArgumentException">If the passed control isn't of a supported type</exception>
public bool IsUp(object control, int index = -1) {
if (control is Keys key)
return this.IsKeyUp(key);
@ -293,6 +460,14 @@ namespace MLEM.Input {
throw new ArgumentException(nameof(control));
}
/// <summary>
/// Returns if a given control of any kind is pressed.
/// This is a helper function that can be passed a <see cref="Keys"/>, <see cref="Buttons"/> or <see cref="MouseButton"/>.
/// </summary>
/// <param name="control">The control whose pressed state to query</param>
/// <param name="index">The index of the gamepad to query (if applicable), or -1 for any gamepad</param>
/// <returns>Whether the given control is down</returns>
/// <exception cref="ArgumentException">If the passed control isn't of a supported type</exception>
public bool IsPressed(object control, int index = -1) {
if (control is Keys key)
return this.IsKeyPressed(key);
@ -303,18 +478,26 @@ namespace MLEM.Input {
throw new ArgumentException(nameof(control));
}
/// <inheritdoc cref="IsDown"/>
public bool IsAnyDown(params object[] control) {
return control.Any(c => this.IsDown(c));
}
/// <inheritdoc cref="IsUp"/>
public bool IsAnyUp(params object[] control) {
return control.Any(c => this.IsUp(c));
}
/// <inheritdoc cref="IsPressed"/>
public bool IsAnyPressed(params object[] control) {
return control.Any(c => this.IsPressed(c));
}
/// <summary>
/// Helper function to enable gestures for a <see cref="TouchPanel"/> easily.
/// Note that, if other gestures were previously enabled, they will not get overridden.
/// </summary>
/// <param name="gestures">The gestures to enable</param>
public static void EnableGestures(params GestureType[] gestures) {
foreach (var gesture in gestures)
TouchPanel.EnabledGestures |= gesture;

View file

@ -4,10 +4,21 @@ using Microsoft.Xna.Framework.Input;
using MLEM.Misc;
namespace MLEM.Input {
/// <summary>
/// A set of extension methods for dealing with <see cref="Keys"/> and <see cref="Keyboard"/>
/// </summary>
public static class KeysExtensions {
/// <summary>
/// All enum values of <see cref="ModifierKey"/>
/// </summary>
public static readonly ModifierKey[] ModifierKeys = EnumHelper.GetValues<ModifierKey>().ToArray();
/// <summary>
/// Returns all of the keys that the given modifier key represents
/// </summary>
/// <param name="modifier">The modifier key</param>
/// <returns>All of the keys the modifier key represents</returns>
public static IEnumerable<Keys> GetKeys(this ModifierKey modifier) {
switch (modifier) {
case ModifierKey.Shift:
@ -25,6 +36,12 @@ namespace MLEM.Input {
}
}
/// <summary>
/// Returns the modifier key that the given key represents.
/// If there is no matching modifier key, <see cref="ModifierKey.None"/> is returned.
/// </summary>
/// <param name="key">The key to convert to a modifier key</param>
/// <returns>The modifier key, or <see cref="ModifierKey.None"/></returns>
public static ModifierKey GetModifier(this Keys key) {
foreach (var mod in ModifierKeys) {
if (GetKeys(mod).Contains(key))
@ -33,17 +50,38 @@ namespace MLEM.Input {
return ModifierKey.None;
}
/// <summary>
/// Returns whether the given key is a modifier key or not.
/// </summary>
/// <param name="key">The key</param>
/// <returns>If the key is a modifier key</returns>
public static bool IsModifier(this Keys key) {
return GetModifier(key) != ModifierKey.None;
}
}
/// <summary>
/// An enum representing modifier keys.
/// A modifier key is a key that is usually pressed as part of key combination to change the function of a regular key.
/// </summary>
public enum ModifierKey {
/// <summary>
/// No modifier key. Only used for <see cref="KeysExtensions.GetModifier"/>
/// </summary>
None,
/// <summary>
/// The shift modifier key. This represents Left Shift and Right Shift keys.
/// </summary>
Shift,
/// <summary>
/// The control modifier key. This represents Left Control and Right Control.
/// </summary>
Control,
/// <summary>
/// The alt modifier key. This represents Alt and Alt Graph.
/// </summary>
Alt
}

View file

@ -4,10 +4,23 @@ using Microsoft.Xna.Framework.Input;
using MLEM.Misc;
namespace MLEM.Input {
/// <summary>
/// A set of extension methods for dealing with <see cref="MouseButton"/> and <see cref="Mouse"/>
/// </summary>
public static class MouseExtensions {
/// <summary>
/// All enum values of <see cref="MouseButton"/>
/// </summary>
public static readonly MouseButton[] MouseButtons = EnumHelper.GetValues<MouseButton>().ToArray();
/// <summary>
/// Returns the <see cref="ButtonState"/> of the given mouse button.
/// </summary>
/// <param name="state">The mouse's current state</param>
/// <param name="button">The button whose state to query</param>
/// <returns>The state of the button</returns>
/// <exception cref="ArgumentException">If a mouse button out of range is passed</exception>
public static ButtonState GetState(this MouseState state, MouseButton button) {
switch (button) {
case MouseButton.Left:
@ -27,12 +40,31 @@ namespace MLEM.Input {
}
/// <summary>
/// This enum is a list of possible mouse buttons.
/// It serves as a wrapper around <see cref="MouseState"/>'s button properties.
/// </summary>
public enum MouseButton {
/// <summary>
/// The left mouse button, or <see cref="MouseState.LeftButton"/>
/// </summary>
Left,
/// <summary>
/// The middle mouse button, or <see cref="MouseState.MiddleButton"/>
/// </summary>
Middle,
/// <summary>
/// The right mouse button, or <see cref="MouseState.RightButton"/>
/// </summary>
Right,
/// <summary>
/// The first extra mouse button, or <see cref="MouseState.XButton1"/>
/// </summary>
Extra1,
/// <summary>
/// The second extra mouse button, or <see cref="MouseState.XButton2"/>
/// </summary>
Extra2
}

View file

@ -2,8 +2,29 @@ using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace MLEM.Misc {
/// <summary>
/// This class contains a <see cref="DrawAutoTile"/> method that allows users to easily draw a tile with automatic connections.
/// For auto-tiling in this manner to work, auto-tiled textures have to be laid out in a format described in <see cref="DrawAutoTile"/>.
/// </summary>
public class AutoTiling {
/// <summary>
/// This method allows for a tiled texture to be drawn in an auto-tiling mode.
/// This allows, for example, a grass patch on a tilemap to have nice looking edges that transfer over into a path without any hard edges between tiles.
///
/// For auto-tiling in this way to work, the tiles have to be laid out in a specific order. This order is shown in the auto-tiling demo's textures.
/// For more information and an example, see <see href="https://github.com/Ellpeck/MLEM/blob/master/Demos/AutoTilingDemo.cs#L20-L28"/>
/// </summary>
/// <param name="batch"></param>
/// <param name="pos"></param>
/// <param name="texture"></param>
/// <param name="textureRegion"></param>
/// <param name="connectsTo"></param>
/// <param name="color"></param>
/// <param name="rotation"></param>
/// <param name="origin"></param>
/// <param name="scale"></param>
/// <param name="layerDepth"></param>
public static void DrawAutoTile(SpriteBatch batch, Vector2 pos, Texture2D texture, Rectangle textureRegion, ConnectsTo connectsTo, Color color, float rotation = 0, Vector2? origin = null, Vector2? scale = null, float layerDepth = 0) {
var org = origin ?? Vector2.Zero;
var sc = scale ?? Vector2.One;
@ -26,6 +47,11 @@ namespace MLEM.Misc {
batch.Draw(texture, new Vector2(pos.X + 0.5F * size.X * sc.X, pos.Y + 0.5F * size.Y * sc.Y), new Rectangle(textureRegion.X + halfSize.X + xDr * size.X, textureRegion.Y + halfSize.Y, halfSize.X, halfSize.Y), color, rotation, org, sc, SpriteEffects.None, layerDepth);
}
/// <summary>
/// A delegate function that determines if a given offset position connects to an auto-tile location.
/// </summary>
/// <param name="xOff">The x offset</param>
/// <param name="yOff">The y offset</param>
public delegate bool ConnectsTo(int xOff, int yOff);
}

View file

@ -5,37 +5,100 @@ using System.Linq;
using Microsoft.Xna.Framework;
namespace MLEM.Misc {
/// <summary>
/// An enum that represents two-dimensional directions.
/// Both straight and diagonal directions are supported.
/// </summary>
public enum Direction2 {
/// <summary>
/// The up direction, or -y.
/// </summary>
Up,
/// <summary>
/// The right direction, or +x.
/// </summary>
Right,
/// <summary>
/// The down direction, or +y.
/// </summary>
Down,
/// <summary>
/// The left direction, or -x.
/// </summary>
Left,
/// <summary>
/// The up and right direction, or +x, -y.
/// </summary>
UpRight,
/// <summary>
/// The down and right direction, or +x, +y.
/// </summary>
DownRight,
/// <summary>
/// The down and left direction, or -x, +y.
/// </summary>
DownLeft,
/// <summary>
/// The up and left direction, or -x, -y.
/// </summary>
UpLeft,
/// <summary>
/// No direction.
/// </summary>
None
}
/// <summary>
/// A set of helper and extension methods for dealing with <see cref="Direction2"/>
/// </summary>
public static class Direction2Helper {
/// <summary>
/// All <see cref="Direction2"/> enum values
/// </summary>
public static readonly Direction2[] All = EnumHelper.GetValues<Direction2>().ToArray();
/// <summary>
/// The <see cref="Direction2.Up"/> through <see cref="Direction2.Left"/> directions
/// </summary>
public static readonly Direction2[] Adjacent = All.Where(IsAdjacent).ToArray();
/// <summary>
/// The <see cref="Direction2.UpRight"/> through <see cref="Direction2.UpLeft"/> directions
/// </summary>
public static readonly Direction2[] Diagonals = All.Where(IsDiagonal).ToArray();
/// <summary>
/// All directions except <see cref="Direction2.None"/>
/// </summary>
public static readonly Direction2[] AllExceptNone = All.Where(dir => dir != Direction2.None).ToArray();
/// <summary>
/// Returns if the given direction is considered an "adjacent" direction.
/// An adjacent direction is one that is not a diagonal.
/// </summary>
/// <param name="dir">The direction to query</param>
/// <returns>Whether the direction is adjacent</returns>
public static bool IsAdjacent(this Direction2 dir) {
return dir <= Direction2.Left;
}
/// <summary>
/// Returns if the given direction is considered a diagonal direction.
/// </summary>
/// <param name="dir">The direction to query</param>
/// <returns>Whether the direction is diagonal</returns>
public static bool IsDiagonal(this Direction2 dir) {
return dir >= Direction2.UpRight && dir <= Direction2.UpLeft;
}
/// <summary>
/// Returns the directional offset of a given direction.
/// The offset direction will be exactly one unit in each axis that the direction represents.
/// </summary>
/// <param name="dir">The direction whose offset to query</param>
/// <returns>The direction's offset</returns>
public static Point Offset(this Direction2 dir) {
switch (dir) {
case Direction2.Up:
@ -59,11 +122,23 @@ namespace MLEM.Misc {
}
}
/// <summary>
/// Maps each direction in the given enumerable of directions to its <see cref="Offset"/>.
/// </summary>
/// <param name="directions">The direction enumerable</param>
/// <returns>The directions' offsets, in the same order as the input directions.</returns>
public static IEnumerable<Point> Offsets(this IEnumerable<Direction2> directions) {
foreach (var dir in directions)
yield return dir.Offset();
}
/// <summary>
/// Returns the opposite of the given direction.
/// For "adjacent" directions, this is the direction that points into the same axis, but the opposite sign.
/// For diagonal directions, this is the direction that points into the opposite of both the x and y axis.
/// </summary>
/// <param name="dir">The direction whose opposite to get</param>
/// <returns>The opposite of the direction</returns>
public static Direction2 Opposite(this Direction2 dir) {
switch (dir) {
case Direction2.Up:
@ -87,11 +162,22 @@ namespace MLEM.Misc {
}
}
/// <summary>
/// Returns the angle of the direction in radians, where <see cref="Direction2.Right"/> has an angle of 0.
/// </summary>
/// <param name="dir">The direction whose angle to get</param>
/// <returns>The direction's angle</returns>
public static float Angle(this Direction2 dir) {
var offset = dir.Offset();
return (float) Math.Atan2(offset.Y, offset.X);
}
/// <summary>
/// Rotates the given direction clockwise and returns the resulting direction.
/// </summary>
/// <param name="dir">The direction to rotate</param>
/// <param name="fortyFiveDegrees">Whether to rotate by 45 degrees. If this is false, the rotation is 90 degrees instead.</param>
/// <returns>The rotated direction</returns>
public static Direction2 RotateCw(this Direction2 dir, bool fortyFiveDegrees = false) {
switch (dir) {
case Direction2.Up:
@ -115,6 +201,12 @@ namespace MLEM.Misc {
}
}
/// <summary>
/// Rotates the given direction counter-clockwise and returns the resulting direction.
/// </summary>
/// <param name="dir">The direction to rotate counter-clockwise</param>
/// <param name="fortyFiveDegrees">Whether to rotate by 45 degrees. If this is false, the rotation is 90 degrees instead.</param>
/// <returns>The rotated direction</returns>
public static Direction2 RotateCcw(this Direction2 dir, bool fortyFiveDegrees = false) {
switch (dir) {
case Direction2.Up:

View file

@ -4,11 +4,25 @@ using System.Linq;
using Microsoft.Xna.Framework.Input;
namespace MLEM.Misc {
/// <summary>
/// A helper class that allows easier usage of <see cref="Enum"/> values.
/// </summary>
public static class EnumHelper {
/// <summary>
/// All values of the <see cref="Buttons"/> enum.
/// </summary>
public static readonly Buttons[] Buttons = GetValues<Buttons>().ToArray();
/// <summary>
/// All values of the <see cref="Keys"/> enum.
/// </summary>
public static readonly Keys[] Keys = GetValues<Keys>().ToArray();
/// <summary>
/// Returns all of the values of the given enum type.
/// </summary>
/// <typeparam name="T">The type whose enum to get</typeparam>
/// <returns>An enumerable of the values of the enum, in declaration order.</returns>
public static IEnumerable<T> GetValues<T>() {
return Enum.GetValues(typeof(T)).Cast<T>();
}

View file

@ -2,10 +2,19 @@ using System;
using System.Collections.Generic;
namespace MLEM.Misc {
/// <summary>
/// Represents an object that can hold generic key-value based data.
/// A lot of MLEM components extend this class to allow for users to add additional data to them easily.
/// </summary>
public class GenericDataHolder {
private Dictionary<string, object> data;
/// <summary>
/// Store a piece of generic data on this object.
/// </summary>
/// <param name="key">The key to store the data by</param>
/// <param name="data">The data to store in the object</param>
public void SetData(string key, object data) {
if (data == default) {
if (this.data != null)
@ -17,12 +26,22 @@ namespace MLEM.Misc {
}
}
/// <summary>
/// Returns a piece of generic data of the given type on this object.
/// </summary>
/// <param name="key">The key that the data is stored by</param>
/// <typeparam name="T">The type of the data stored</typeparam>
/// <returns>The data, or default if it doesn't exist</returns>
public T GetData<T>(string key) {
if (this.data != null && this.data.TryGetValue(key, out var val) && val is T t)
return t;
return default;
}
/// <summary>
/// Returns all of the generic data that this object stores.
/// </summary>
/// <returns>The generic data on this object</returns>
public IReadOnlyCollection<string> GetDataKeys() {
if (this.data == null)
return Array.Empty<string>();

View file

@ -1,15 +1,44 @@
using Microsoft.Xna.Framework;
namespace MLEM.Misc {
/// <summary>
/// Represents a generic padding.
/// A padding is an object of data that stores an offset from each side of a rectangle or square.
/// </summary>
public struct Padding {
/// <summary>
/// The amount of padding on the left side
/// </summary>
public float Left;
/// <summary>
/// The amount of padding on the right side
/// </summary>
public float Right;
/// <summary>
/// The amount of padding on the top side
/// </summary>
public float Top;
/// <summary>
/// The amount of padding on the bottom side
/// </summary>
public float Bottom;
/// <summary>
/// The total width of this padding, a sum of the left and right padding.
/// </summary>
public float Width => this.Left + this.Right;
/// <summary>
/// The total height of this padding, a sum of the top and bottom padding.
/// </summary>
public float Height => this.Top + this.Bottom;
/// <summary>
/// Create a new padding with the specified borders.
/// </summary>
/// <param name="left">The amount of padding on the left side</param>
/// <param name="right">The amount of padding on the right side</param>
/// <param name="top">The amount of padding on the top side</param>
/// <param name="bottom">The amount of padding on the bottom side</param>
public Padding(float left, float right, float top, float bottom) {
this.Left = left;
this.Right = right;
@ -17,38 +46,58 @@ namespace MLEM.Misc {
this.Bottom = bottom;
}
/// <summary>
/// Implicitly creates a padding from the given two-dimensional vector.
/// The left and right padding will both be the vector's x value, and the top and bottom padding will both be the vector's y value.
/// </summary>
/// <param name="vec">The vector to convert</param>
/// <returns>A padding based on the vector's values</returns>
public static implicit operator Padding(Vector2 vec) {
return new Padding(vec.X, vec.X, vec.Y, vec.Y);
}
/// <summary>
/// Scales a padding by a scalar.
/// </summary>
public static Padding operator *(Padding p, float scale) {
return new Padding(p.Left * scale, p.Right * scale, p.Top * scale, p.Bottom * scale);
}
/// <summary>
/// Adds two paddings together in a memberwise fashion.
/// </summary>
public static Padding operator +(Padding left, Padding right) {
return new Padding(left.Left + right.Left, left.Right + right.Right, left.Top + right.Top, left.Bottom + right.Bottom);
}
/// <summary>
/// Subtracts two paddings in a memberwise fashion.
/// </summary>
public static Padding operator -(Padding left, Padding right) {
return new Padding(left.Left - right.Left, left.Right - right.Right, left.Top - right.Top, left.Bottom - right.Bottom);
}
/// <inheritdoc cref="Equals(Padding)"/>
public static bool operator ==(Padding left, Padding right) {
return left.Equals(right);
}
/// <inheritdoc cref="Equals(Padding)"/>
public static bool operator !=(Padding left, Padding right) {
return !(left == right);
}
/// <inheritdoc cref="Equals(object)"/>
public bool Equals(Padding other) {
return this.Left.Equals(other.Left) && this.Right.Equals(other.Right) && this.Top.Equals(other.Top) && this.Bottom.Equals(other.Bottom);
}
/// <inheritdoc />
public override bool Equals(object obj) {
return obj is Padding other && this.Equals(other);
}
/// <inheritdoc />
public override int GetHashCode() {
var hashCode = this.Left.GetHashCode();
hashCode = (hashCode * 397) ^ this.Right.GetHashCode();

View file

@ -4,26 +4,59 @@ using Microsoft.Xna.Framework;
using MLEM.Extensions;
namespace MLEM.Misc {
/// <summary>
/// Represents a float-based version of <see cref="Rectangle"/>
/// </summary>
[DataContract]
public struct RectangleF : IEquatable<RectangleF> {
/// <summary>
/// The empty rectangle, with an x, y, width and height of 0.
/// </summary>
public static RectangleF Empty => default;
/// <summary>
/// The x position of the top left corner of this rectangle.
/// </summary>
[DataMember]
public float X;
/// <summary>
/// The y position of the top left corner of this rectangle.
/// </summary>
[DataMember]
public float Y;
/// <summary>
/// The width of this rectangle.
/// </summary>
[DataMember]
public float Width;
/// <summary>
/// The height of this rectangle.
/// </summary>
[DataMember]
public float Height;
/// <inheritdoc cref="X"/>
public float Left => this.X;
/// <summary>
/// The x position of the bottom right corner of this rectangle.
/// </summary>
public float Right => this.X + this.Width;
/// <inheritdoc cref="Y"/>
public float Top => this.Y;
/// <summary>
/// The y position of the bottom right corner of this rectangle.
/// </summary>
public float Bottom => this.Y + this.Height;
/// <summary>
/// A boolean that is true if this rectangle is empty.
/// A rectangle is considered empty if the width or height is 0.
/// </summary>
public bool IsEmpty => this.Width <= 0 || this.Height <= 0;
/// <summary>
/// The top left corner of this rectangle
/// </summary>
public Vector2 Location {
get => new Vector2(this.X, this.Y);
set {
@ -31,6 +64,9 @@ namespace MLEM.Misc {
this.Y = value.Y;
}
}
/// <summary>
/// The size, that is, the width and height of this rectangle.
/// </summary>
public Vector2 Size {
get => new Vector2(this.Width, this.Height);
set {
@ -38,8 +74,18 @@ namespace MLEM.Misc {
this.Height = value.Y;
}
}
/// <summary>
/// The center of this rectangle, based on the top left corner and the size.
/// </summary>
public Vector2 Center => new Vector2(this.X + this.Width / 2, this.Y + this.Height / 2);
/// <summary>
/// Creates a new rectangle with the specified location and size
/// </summary>
/// <param name="x">The x coordinate of the top left corner of the rectangle</param>
/// <param name="y">The y coordinate of the top left corner of the rectangle</param>
/// <param name="width">The width of the rectangle</param>
/// <param name="height">The height of the rectangle</param>
public RectangleF(float x, float y, float width, float height) {
this.X = x;
this.Y = y;
@ -47,6 +93,11 @@ namespace MLEM.Misc {
this.Height = height;
}
/// <summary>
/// Creates a new rectangle with the specified location and size vectors
/// </summary>
/// <param name="location">The top left corner of the rectangle</param>
/// <param name="size">The size of the rectangle, where x represents width and the y represents height</param>
public RectangleF(Vector2 location, Vector2 size) {
this.X = location.X;
this.Y = location.Y;
@ -54,6 +105,13 @@ namespace MLEM.Misc {
this.Height = size.Y;
}
/// <summary>
/// Creates a new rectangle based on two corners that form a bounding box.
/// The resulting rectangle will encompass both corners as well as all of the space between them.
/// </summary>
/// <param name="corner1">The first corner to use</param>
/// <param name="corner2">The second corner to use</param>
/// <returns></returns>
public static RectangleF FromCorners(Vector2 corner1, Vector2 corner2) {
var minX = Math.Min(corner1.X, corner2.X);
var minY = Math.Min(corner1.Y, corner2.Y);
@ -62,46 +120,65 @@ namespace MLEM.Misc {
return new RectangleF(minX, minY, maxX - minX, maxY - minY);
}
/// <summary>
/// Converts a float-based rectangle to an int-based rectangle, flooring each value in the process.
/// </summary>
/// <param name="rect">The rectangle to convert</param>
/// <returns>The resulting rectangle</returns>
public static explicit operator Rectangle(RectangleF rect) {
return new Rectangle(rect.X.Floor(), rect.Y.Floor(), rect.Width.Floor(), rect.Height.Floor());
}
/// <summary>
/// Converts an int-based rectangle to a float-based rectangle.
/// </summary>
/// <param name="rect">The rectangle to convert</param>
/// <returns>The resulting rectangle</returns>
public static explicit operator RectangleF(Rectangle rect) {
return new RectangleF(rect.X, rect.Y, rect.Width, rect.Height);
}
/// <inheritdoc cref="Equals(RectangleF)"/>
public static bool operator ==(RectangleF a, RectangleF b) {
return a.X == b.X && a.Y == b.Y && a.Width == b.Width && a.Height == b.Height;
}
/// <inheritdoc cref="Equals(RectangleF)"/>
public static bool operator !=(RectangleF a, RectangleF b) {
return !(a == b);
}
/// <inheritdoc cref="Rectangle.Contains(float, float)"/>
public bool Contains(float x, float y) {
return this.X <= x && x < this.X + this.Width && this.Y <= y && y < this.Y + this.Height;
}
/// <inheritdoc cref="Rectangle.Contains(Vector2)"/>
public bool Contains(Vector2 value) {
return this.Contains(value.X, value.Y);
}
/// <inheritdoc cref="Rectangle.Contains(Rectangle)"/>
public bool Contains(RectangleF value) {
return this.X <= value.X && value.X + value.Width <= this.X + this.Width && this.Y <= value.Y && value.Y + value.Height <= this.Y + this.Height;
}
/// <inheritdoc />
public override bool Equals(object obj) {
return obj is RectangleF f && this == f;
}
/// <inheritdoc />
public bool Equals(RectangleF other) {
return this == other;
}
/// <inheritdoc />
public override int GetHashCode() {
return (((17 * 23 + this.X.GetHashCode()) * 23 + this.Y.GetHashCode()) * 23 + this.Width.GetHashCode()) * 23 + this.Height.GetHashCode();
}
/// <inheritdoc cref="Rectangle.Inflate(float,float)"/>
public void Inflate(float horizontalAmount, float verticalAmount) {
this.X -= horizontalAmount;
this.Y -= verticalAmount;
@ -109,10 +186,12 @@ namespace MLEM.Misc {
this.Height += verticalAmount * 2;
}
/// <inheritdoc cref="Rectangle.Intersects(Rectangle)"/>
public bool Intersects(RectangleF value) {
return value.Left < this.Right && this.Left < value.Right && value.Top < this.Bottom && this.Top < value.Bottom;
}
/// <inheritdoc cref="Rectangle.Intersect(Rectangle, Rectangle)"/>
public static RectangleF Intersect(RectangleF value1, RectangleF value2) {
if (value1.Intersects(value2)) {
var num1 = Math.Min(value1.X + value1.Width, value2.X + value2.Width);
@ -125,26 +204,31 @@ namespace MLEM.Misc {
}
}
/// <inheritdoc cref="Rectangle.Offset(float, float)"/>
public void Offset(float offsetX, float offsetY) {
this.X += offsetX;
this.Y += offsetY;
}
/// <inheritdoc cref="Rectangle.Offset(Vector2)"/>
public void Offset(Vector2 amount) {
this.X += amount.X;
this.Y += amount.Y;
}
/// <inheritdoc/>
public override string ToString() {
return "{X:" + this.X + " Y:" + this.Y + " Width:" + this.Width + " Height:" + this.Height + "}";
}
/// <inheritdoc cref="Rectangle.Union(Rectangle, Rectangle)"/>
public static RectangleF Union(RectangleF value1, RectangleF value2) {
var x = Math.Min(value1.X, value2.X);
var y = Math.Min(value1.Y, value2.Y);
return new RectangleF(x, y, Math.Max(value1.Right, value2.Right) - x, Math.Max(value1.Bottom, value2.Bottom) - y);
}
/// <inheritdoc cref="Rectangle.Deconstruct"/>
public void Deconstruct(out float x, out float y, out float width, out float height) {
x = this.X;
y = this.Y;

View file

@ -6,9 +6,19 @@ using Microsoft.Xna.Framework.Input;
using MLEM.Input;
namespace MLEM.Misc {
/// <summary>
/// A text input wrapper is a wrapper around MonoGame's built-in text input event.
/// Since said text input event does not exist on non-Desktop devices, we want to wrap it in a wrapper that is platform-independent for MLEM.
/// See subclasses of this wrapper or <see href="https://mlem.ellpeck.de/articles/ui.html#text-input"/> for more info.
/// </summary>
public abstract class TextInputWrapper {
private static TextInputWrapper current;
/// <summary>
/// The current text input wrapper.
/// Set this value before starting your game if you want to use text input wrapping.
/// </summary>
/// <exception cref="InvalidOperationException"></exception>
public static TextInputWrapper Current {
get {
if (current == null)
@ -18,26 +28,59 @@ namespace MLEM.Misc {
set => current = value;
}
/// <summary>
/// Determines if this text input wrapper requires an on-screen keyboard.
/// </summary>
/// <returns>If this text input wrapper requires an on-screen keyboard</returns>
public abstract bool RequiresOnScreenKeyboard();
/// <summary>
/// Adds a text input listener to this text input wrapper.
/// The supplied listener will be called whenever a character is input.
/// </summary>
/// <param name="window">The game's window</param>
/// <param name="callback">The callback that should be called whenever a character is pressed</param>
public abstract void AddListener(GameWindow window, TextInputCallback callback);
/// <summary>
/// A delegate method that can be used for <see cref="TextInputWrapper.AddListener"/>
/// </summary>
/// <param name="sender">The object that sent the event. The <see cref="TextInputWrapper"/> used in most cases.</param>
/// <param name="key">The key that was pressed</param>
/// <param name="character">The character that corresponds to that key</param>
public delegate void TextInputCallback(object sender, Keys key, char character);
/// <summary>
/// A text input wrapper for DesktopGL devices.
/// This wrapper uses the built-in MonoGame TextInput event, which makes this listener work with any keyboard localization natively.
/// </summary>
/// <example>
/// This listener is initialized as follows:
/// <code>
/// new TextInputWrapper.DesktopGl{TextInputEventArgs}((w, c) => w.TextInput += c);
/// </code>
/// </example>
/// <typeparam name="T"></typeparam>
public class DesktopGl<T> : TextInputWrapper {
private MemberInfo key;
private MemberInfo character;
private readonly Action<GameWindow, EventHandler<T>> addListener;
/// <summary>
/// Creates a new DesktopGL-based text input wrapper
/// </summary>
/// <param name="addListener">The function that is used to add a text input listener</param>
public DesktopGl(Action<GameWindow, EventHandler<T>> addListener) {
this.addListener = addListener;
}
/// <inheritdoc />
public override bool RequiresOnScreenKeyboard() {
return false;
}
/// <inheritdoc />
public override void AddListener(GameWindow window, TextInputCallback callback) {
this.addListener(window, (sender, args) => {
// the old versions of DesktopGL use a property here, while the
@ -69,32 +112,54 @@ namespace MLEM.Misc {
}
/// <summary>
/// A text input wrapper for mobile platforms as well as consoles.
/// This text input wrapper performs no actions itself, as it signals that an on-screen keyboard is required.
/// </summary>
public class Mobile : TextInputWrapper {
/// <inheritdoc />
public override bool RequiresOnScreenKeyboard() {
return true;
}
/// <inheritdoc />
public override void AddListener(GameWindow window, TextInputCallback callback) {
}
}
/// <summary>
/// A text input wrapper that does nothing.
/// This can be used if no text input is required for the game.
/// </summary>
public class None : TextInputWrapper {
/// <inheritdoc />
public override bool RequiresOnScreenKeyboard() {
return false;
}
/// <inheritdoc />
public override void AddListener(GameWindow window, TextInputCallback callback) {
}
}
/// <summary>
/// A primitive text input wrapper that is locked to the American keyboard localization.
/// Only use this text input wrapper if <see cref="TextInputWrapper.DesktopGl{T}"/> is unavailable for some reason.
///
/// Note that, when using this text input wrapper, its <see cref="Update"/> method has to be called periodically.
/// </summary>
public class Primitive : TextInputWrapper {
private TextInputCallback callback;
/// <summary>
/// Updates this text input wrapper by querying pressed keys and sending corresponding input events.
/// </summary>
/// <param name="handler">The input handler to use for text input querying</param>
public void Update(InputHandler handler) {
var pressed = handler.KeyboardState.GetPressedKeys().Except(handler.LastKeyboardState.GetPressedKeys());
var shift = handler.IsModifierKeyDown(ModifierKey.Shift);
@ -105,10 +170,12 @@ namespace MLEM.Misc {
}
}
/// <inheritdoc />
public override bool RequiresOnScreenKeyboard() {
return false;
}
/// <inheritdoc />
public override void AddListener(GameWindow window, TextInputCallback callback) {
this.callback += callback;
}

View file

@ -4,18 +4,61 @@ using System.Diagnostics;
using System.Threading.Tasks;
namespace MLEM.Pathfinding {
/// <summary>
/// This is an abstract implementation of the A* path finding algorithm.
/// This implementation is used by <see cref="AStar2"/>, a 2-dimensional A* path finding algorithm, and <see cref="AStar3"/>, a 3-dimensional A* path finding algorithm.
/// </summary>
/// <typeparam name="T">The type of points used for this path</typeparam>
public abstract class AStar<T> {
/// <summary>
/// A value that represents an infinite path cost, or a cost for a location that cannot possibly be reached.
/// </summary>
public static readonly float InfiniteCost = float.PositiveInfinity;
/// <summary>
/// The array of all directions that will be checked for path finding.
/// Note that this array is only used if <see cref="DefaultAllowDiagonals"/> is true.
/// </summary>
public readonly T[] AllDirections;
/// <summary>
/// The array of all adjacent directions that will be checked for path finding.
/// Note that this array is only used if <see cref="DefaultAllowDiagonals"/> is false.
/// </summary>
public readonly T[] AdjacentDirections;
/// <summary>
/// The default cost function that determines the cost for each path finding position.
/// </summary>
public GetCost DefaultCostFunction;
/// <summary>
/// The default cost for a path point.
/// </summary>
public float DefaultCost;
/// <summary>
/// The default amount of maximum tries that will be used before path finding is aborted.
/// </summary>
public int DefaultMaxTries;
/// <summary>
/// Whether or not diagonal directions are considered while finding a path.
/// </summary>
public bool DefaultAllowDiagonals;
/// <summary>
/// The amount of tries required for finding the last queried path
/// </summary>
public int LastTriesNeeded { get; private set; }
/// <summary>
/// The amount of time required for finding the last queried path
/// </summary>
public TimeSpan LastTimeNeeded { get; private set; }
/// <summary>
/// Creates a new A* pathfinder with the supplied default settings.
/// </summary>
/// <param name="allDirections">All directions that should be checked</param>
/// <param name="adjacentDirections">All adjacent directions that should be checked</param>
/// <param name="defaultCostFunction">The default function for cost determination of a path point</param>
/// <param name="defaultAllowDiagonals">Whether or not diagonals should be allowed by default</param>
/// <param name="defaultCost">The default cost for a path point</param>
/// <param name="defaultMaxTries">The default amount of tries before path finding is aborted</param>
protected AStar(T[] allDirections, T[] adjacentDirections, GetCost defaultCostFunction, bool defaultAllowDiagonals, float defaultCost = 1, int defaultMaxTries = 10000) {
this.AllDirections = allDirections;
this.AdjacentDirections = adjacentDirections;
@ -25,10 +68,21 @@ namespace MLEM.Pathfinding {
this.DefaultAllowDiagonals = defaultAllowDiagonals;
}
/// <inheritdoc cref="FindPath"/>
public Task<Stack<T>> FindPathAsync(T start, T goal, GetCost costFunction = null, float? defaultCost = null, int? maxTries = null, bool? allowDiagonals = null) {
return Task.Run(() => this.FindPath(start, goal, costFunction, defaultCost, maxTries, allowDiagonals));
}
/// <summary>
/// Finds a path between two points using this pathfinder's default settings or, alternatively, the supplied override settings.
/// </summary>
/// <param name="start">The point to start path finding at</param>
/// <param name="goal">The point to find a path to</param>
/// <param name="costFunction">The function that determines the cost for each path point</param>
/// <param name="defaultCost">The default cost for each path point</param>
/// <param name="maxTries">The maximum amount of tries before path finding is aborted</param>
/// <param name="allowDiagonals">If diagonals should be looked at for path finding</param>
/// <returns>A stack of path points, where the top item is the first point to go to.</returns>
public Stack<T> FindPath(T start, T goal, GetCost costFunction = null, float? defaultCost = null, int? maxTries = null, bool? allowDiagonals = null) {
var stopwatch = Stopwatch.StartNew();
@ -94,8 +148,14 @@ namespace MLEM.Pathfinding {
return ret;
}
/// <summary>
/// A helper method to add two positions together.
/// </summary>
protected abstract T AddPositions(T first, T second);
/// <summary>
/// A helper method to get the Manhattan Distance between two points.
/// </summary>
protected abstract float GetManhattanDistance(T first, T second);
private static Stack<T> CompilePath(PathPoint<T> current) {
@ -107,17 +167,48 @@ namespace MLEM.Pathfinding {
return path;
}
/// <summary>
/// A cost function for a given path finding position.
/// If a path point should have the default cost, <see cref="AStar{T}.DefaultCost"/> should be returned.
/// If a path point should be unreachable, <see cref="AStar{T}.InfiniteCost"/> should be returned.
/// </summary>
/// <param name="currPos">The current position in the path</param>
/// <param name="nextPos">The position we're trying to reach from the current position</param>
public delegate float GetCost(T currPos, T nextPos);
}
/// <summary>
/// A point in a <see cref="AStar{T}"/> path
/// </summary>
/// <typeparam name="T">The type of point used for this path</typeparam>
public class PathPoint<T> {
/// <summary>
/// The path point that this point originated from
/// </summary>
public readonly PathPoint<T> Parent;
/// <summary>
/// The position of this path point
/// </summary>
public readonly T Pos;
/// <summary>
/// The F cost of this path point
/// </summary>
public readonly float F;
/// <summary>
/// The G cost of this path point
/// </summary>
public readonly float G;
/// <summary>
/// Creates a new path point with the supplied settings.
/// </summary>
/// <param name="pos">The point's position</param>
/// <param name="distance">The point's manhattan distance from the start point</param>
/// <param name="parent">The point's parent</param>
/// <param name="terrainCostForThisPos">The point's terrain cost, based on <see cref="AStar{T}.GetCost"/></param>
/// <param name="defaultCost">The default cost for a path point</param>
public PathPoint(T pos, float distance, PathPoint<T> parent, float terrainCostForThisPos, float defaultCost) {
this.Pos = pos;
this.Parent = parent;
@ -126,12 +217,14 @@ namespace MLEM.Pathfinding {
this.F = this.G + distance * defaultCost;
}
/// <inheritdoc />
public override bool Equals(object obj) {
if (obj == this)
return true;
return obj is PathPoint<T> point && point.Pos.Equals(this.Pos);
}
/// <inheritdoc />
public override int GetHashCode() {
return this.Pos.GetHashCode();
}

View file

@ -4,19 +4,25 @@ using Microsoft.Xna.Framework;
using MLEM.Misc;
namespace MLEM.Pathfinding {
/// <summary>
/// A 2-dimensional implementation of <see cref="AStar{T}"/> that uses <see cref="Point"/> for positions.
/// </summary>
public class AStar2 : AStar<Point> {
private static readonly Point[] AdjacentDirs = Direction2Helper.Adjacent.Offsets().ToArray();
private static readonly Point[] AllDirs = Direction2Helper.All.Offsets().ToArray();
/// <inheritdoc />
public AStar2(GetCost defaultCostFunction, bool defaultAllowDiagonals, float defaultCost = 1, int defaultMaxTries = 10000) :
base(AllDirs, AdjacentDirs, defaultCostFunction, defaultAllowDiagonals, defaultCost, defaultMaxTries) {
}
/// <inheritdoc />
protected override Point AddPositions(Point first, Point second) {
return first + second;
}
/// <inheritdoc />
protected override float GetManhattanDistance(Point first, Point second) {
return Math.Abs(second.X - first.X) + Math.Abs(second.Y - first.Y);
}

View file

@ -4,6 +4,9 @@ using System.Linq;
using Microsoft.Xna.Framework;
namespace MLEM.Pathfinding {
/// <summary>
/// A 3-dimensional implementation of <see cref="AStar{T}"/> that uses <see cref="Vector3"/> for positions.
/// </summary>
public class AStar3 : AStar<Vector3> {
private static readonly Vector3[] AdjacentDirs = {
@ -31,14 +34,17 @@ namespace MLEM.Pathfinding {
AllDirs = dirs.ToArray();
}
/// <inheritdoc />
public AStar3(GetCost defaultCostFunction, bool defaultAllowDiagonals, float defaultCost = 1, int defaultMaxTries = 10000) :
base(AllDirs, AdjacentDirs, defaultCostFunction, defaultAllowDiagonals, defaultCost, defaultMaxTries) {
}
/// <inheritdoc />
protected override Vector3 AddPositions(Vector3 first, Vector3 second) {
return first + second;
}
/// <inheritdoc />
protected override float GetManhattanDistance(Vector3 first, Vector3 second) {
return Math.Abs(second.X - first.X) + Math.Abs(second.Y - first.Y) + Math.Abs(second.Z - first.Z);
}

View file

@ -6,37 +6,70 @@ using MLEM.Extensions;
using MLEM.Misc;
namespace MLEM.Textures {
/// <summary>
/// This class represents a texture with nine areas.
/// A nine patch texture is useful if a big area should be covered by a small texture that has a specific outline, like a gui panel texture. The center of the texture will be stretched, while the outline of the texture will remain at its original size, keeping aspect ratios alive.
/// </summary>
public class NinePatch : GenericDataHolder {
/// <summary>
/// The texture region of this nine patch
/// </summary>
public readonly TextureRegion Region;
/// <summary>
/// The padding in each direction that marks where the outline area stops
/// </summary>
public readonly Padding Padding;
/// <summary>
/// The nine patches that result from the <see cref="Padding"/>
/// </summary>
public readonly Rectangle[] SourceRectangles;
/// <summary>
/// Creates a new nine patch from a texture and a padding
/// </summary>
/// <param name="texture">The texture to use</param>
/// <param name="padding">The padding that marks where the outline area stops</param>
public NinePatch(TextureRegion texture, Padding padding) {
this.Region = texture;
this.Padding = padding;
this.SourceRectangles = this.CreateRectangles(this.Region.Area).ToArray();
}
/// <summary>
/// Creates a new nine patch from a texture and a padding
/// </summary>
/// <param name="texture">The texture to use</param>
/// <param name="paddingLeft">The padding on the left edge</param>
/// <param name="paddingRight">The padding on the right edge</param>
/// <param name="paddingTop">The padding on the top edge</param>
/// <param name="paddingBottom">The padding on the bottom edge</param>
public NinePatch(TextureRegion texture, int paddingLeft, int paddingRight, int paddingTop, int paddingBottom) :
this(texture, new Padding(paddingLeft, paddingRight, paddingTop, paddingBottom)) {
}
/// <inheritdoc cref="NinePatch(TextureRegion, int, int, int, int)"/>
public NinePatch(Texture2D texture, int paddingLeft, int paddingRight, int paddingTop, int paddingBottom) :
this(new TextureRegion(texture), paddingLeft, paddingRight, paddingTop, paddingBottom) {
}
/// <summary>
/// Creates a new nine patch from a texture and a uniform padding
/// </summary>
/// <param name="texture">The texture to use</param>
/// <param name="padding">The padding that each edge should have</param>
public NinePatch(Texture2D texture, int padding) : this(new TextureRegion(texture), padding) {
}
/// <inheritdoc cref="NinePatch(TextureRegion, int)"/>
public NinePatch(TextureRegion texture, int padding) : this(texture, padding, padding, padding, padding) {
}
public IEnumerable<Rectangle> CreateRectangles(Rectangle area, float patchScale = 1) {
private IEnumerable<Rectangle> CreateRectangles(Rectangle area, float patchScale = 1) {
return this.CreateRectangles((RectangleF) area, patchScale).Select(r => (Rectangle) r);
}
public IEnumerable<RectangleF> CreateRectangles(RectangleF area, float patchScale = 1) {
internal IEnumerable<RectangleF> CreateRectangles(RectangleF area, float patchScale = 1) {
var pl = this.Padding.Left * patchScale;
var pr = this.Padding.Right * patchScale;
var pt = this.Padding.Top * patchScale;
@ -62,8 +95,23 @@ namespace MLEM.Textures {
}
/// <summary>
/// A set of extensions that allow for <see cref="NinePatch"/> rendering
/// </summary>
public static class NinePatchExtensions {
/// <summary>
/// Draws a nine patch area using the given sprite batch
/// </summary>
/// <param name="batch">The batch to draw with</param>
/// <param name="texture">The nine patch to draw</param>
/// <param name="destinationRectangle">The area that should be covered by this nine patch</param>
/// <param name="color">The color to use</param>
/// <param name="rotation">The rotation</param>
/// <param name="origin">The origin position</param>
/// <param name="effects">The effects that the sprite should have</param>
/// <param name="layerDepth">The depth</param>
/// <param name="patchScale">The scale of each area of the nine patch</param>
public static void Draw(this SpriteBatch batch, NinePatch texture, RectangleF destinationRectangle, Color color, float rotation, Vector2 origin, SpriteEffects effects, float layerDepth, float patchScale = 1) {
var dest = texture.CreateRectangles(destinationRectangle, patchScale);
var count = 0;
@ -74,14 +122,17 @@ namespace MLEM.Textures {
}
}
/// <inheritdoc cref="Draw(Microsoft.Xna.Framework.Graphics.SpriteBatch,MLEM.Textures.NinePatch,MLEM.Misc.RectangleF,Microsoft.Xna.Framework.Color,float,Microsoft.Xna.Framework.Vector2,Microsoft.Xna.Framework.Graphics.SpriteEffects,float,float)"/>
public static void Draw(this SpriteBatch batch, NinePatch texture, Rectangle destinationRectangle, Color color, float rotation, Vector2 origin, SpriteEffects effects, float layerDepth, float patchScale = 1) {
batch.Draw(texture, (RectangleF) destinationRectangle, color, rotation, origin, effects, layerDepth, patchScale);
}
/// <inheritdoc cref="Draw(Microsoft.Xna.Framework.Graphics.SpriteBatch,MLEM.Textures.NinePatch,MLEM.Misc.RectangleF,Microsoft.Xna.Framework.Color,float,Microsoft.Xna.Framework.Vector2,Microsoft.Xna.Framework.Graphics.SpriteEffects,float,float)"/>
public static void Draw(this SpriteBatch batch, NinePatch texture, RectangleF destinationRectangle, Color color, float patchScale = 1) {
batch.Draw(texture, destinationRectangle, color, 0, Vector2.Zero, SpriteEffects.None, 0, patchScale);
}
/// <inheritdoc cref="Draw(Microsoft.Xna.Framework.Graphics.SpriteBatch,MLEM.Textures.NinePatch,MLEM.Misc.RectangleF,Microsoft.Xna.Framework.Color,float,Microsoft.Xna.Framework.Vector2,Microsoft.Xna.Framework.Graphics.SpriteEffects,float,float)"/>
public static void Draw(this SpriteBatch batch, NinePatch texture, Rectangle destinationRectangle, Color color, float patchScale = 1) {
batch.Draw(texture, destinationRectangle, color, 0, Vector2.Zero, SpriteEffects.None, 0, patchScale);
}

View file

@ -4,59 +4,119 @@ using MLEM.Extensions;
using MLEM.Misc;
namespace MLEM.Textures {
/// <summary>
/// This class represents a part of a texture.
/// </summary>
public class TextureRegion : GenericDataHolder {
/// <summary>
/// The texture that this region is a part of
/// </summary>
public readonly Texture2D Texture;
/// <summary>
/// The area that is covered by this texture region
/// </summary>
public readonly Rectangle Area;
/// <summary>
/// The top left corner of this texture region
/// </summary>
public Point Position => this.Area.Location;
/// <summary>
/// The x coordinate of the top left corner of this texture region
/// </summary>
public int U => this.Area.X;
/// <summary>
/// The y coordinate of the top left corner of this texture region
/// </summary>
public int V => this.Area.Y;
/// <summary>
/// The size of this texture region
/// </summary>
public Point Size => this.Area.Size;
/// <summary>
/// The width of this texture region
/// </summary>
public int Width => this.Area.Width;
/// <summary>
/// The height of this texture region
/// </summary>
public int Height => this.Area.Height;
/// <summary>
/// Creates a new texture region from a texture and a rectangle which defines the region's area
/// </summary>
/// <param name="texture">The texture to use</param>
/// <param name="area">The area that this texture region should cover</param>
public TextureRegion(Texture2D texture, Rectangle area) {
this.Texture = texture;
this.Area = area;
}
/// <summary>
/// Creates a new texture region that spans the entire texture
/// </summary>
/// <param name="texture">The texture to use</param>
public TextureRegion(Texture2D texture) : this(texture, new Rectangle(0, 0, texture.Width, texture.Height)) {
}
/// <summary>
/// Creates a new texture region based on a texture and area coordinates
/// </summary>
/// <param name="texture">The texture to use</param>
/// <param name="u">The x coordinate of the top left corner of this area</param>
/// <param name="v">The y coordinate of the top left corner of this area</param>
/// <param name="width">The width of this area</param>
/// <param name="height">The height of this area</param>
public TextureRegion(Texture2D texture, int u, int v, int width, int height) : this(texture, new Rectangle(u, v, width, height)) {
}
/// <summary>
/// Creates a new texture region based on a texture, a position and a size
/// </summary>
/// <param name="texture">The texture to use</param>
/// <param name="uv">The top left corner of this area</param>
/// <param name="size">The size of this area</param>
public TextureRegion(Texture2D texture, Point uv, Point size) : this(texture, new Rectangle(uv, size)) {
}
}
/// <summary>
/// This class provides a set of extension methods for dealing with <see cref="TextureRegion"/>
/// </summary>
public static class TextureRegionExtensions {
/// <inheritdoc cref="SpriteBatch.Draw(Texture2D, Vector2, Rectangle?, Color, float, Vector2, Vector2, SpriteEffects, float)"/>
public static void Draw(this SpriteBatch batch, TextureRegion texture, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth) {
batch.Draw(texture.Texture, position, texture.Area, color, rotation, origin, scale, effects, layerDepth);
}
/// <inheritdoc cref="SpriteBatch.Draw(Texture2D, Vector2, Rectangle?, Color, float, Vector2, Vector2, SpriteEffects, float)"/>
public static void Draw(this SpriteBatch batch, TextureRegion texture, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects effects, float layerDepth) {
batch.Draw(texture, position, color, rotation, origin, new Vector2(scale), effects, layerDepth);
}
/// <inheritdoc cref="SpriteBatch.Draw(Texture2D, Vector2, Rectangle?, Color, float, Vector2, Vector2, SpriteEffects, float)"/>
public static void Draw(this SpriteBatch batch, TextureRegion texture, Rectangle destinationRectangle, Color color, float rotation, Vector2 origin, SpriteEffects effects, float layerDepth) {
batch.Draw(texture.Texture, destinationRectangle, texture.Area, color, rotation, origin, effects, layerDepth);
}
/// <inheritdoc cref="SpriteBatch.Draw(Texture2D, Vector2, Rectangle?, Color, float, Vector2, Vector2, SpriteEffects, float)"/>
public static void Draw(this SpriteBatch batch, TextureRegion texture, RectangleF destinationRectangle, Color color, float rotation, Vector2 origin, SpriteEffects effects, float layerDepth) {
batch.Draw(texture.Texture, destinationRectangle, texture.Area, color, rotation, origin, effects, layerDepth);
}
/// <inheritdoc cref="SpriteBatch.Draw(Texture2D, Vector2, Rectangle?, Color, float, Vector2, Vector2, SpriteEffects, float)"/>
public static void Draw(this SpriteBatch batch, TextureRegion texture, Vector2 position, Color color) {
batch.Draw(texture, position, color, 0, Vector2.Zero, Vector2.One, SpriteEffects.None, 0);
}
/// <inheritdoc cref="SpriteBatch.Draw(Texture2D, Vector2, Rectangle?, Color, float, Vector2, Vector2, SpriteEffects, float)"/>
public static void Draw(this SpriteBatch batch, TextureRegion texture, Rectangle destinationRectangle, Color color) {
batch.Draw(texture, destinationRectangle, color, 0, Vector2.Zero, SpriteEffects.None, 0);
}
/// <inheritdoc cref="SpriteBatch.Draw(Texture2D, Vector2, Rectangle?, Color, float, Vector2, Vector2, SpriteEffects, float)"/>
public static void Draw(this SpriteBatch batch, TextureRegion texture, RectangleF destinationRectangle, Color color) {
batch.Draw(texture, destinationRectangle, color, 0, Vector2.Zero, SpriteEffects.None, 0);
}

View file

@ -5,21 +5,63 @@ using Microsoft.Xna.Framework.Graphics;
using MLEM.Misc;
namespace MLEM.Textures {
/// <summary>
/// This class represents an atlas of <see cref="TextureRegion"/> objects that is uniform.
/// Uniform, in this case, means that the texture atlas' size is not determined by the width and height of the texture, but instead by the amount of sub-regions that the atlas has in the x and y direction.
/// Using a uniform texture atlas over a regular texture as an atlas allows for texture artists to create higher resolution textures without coordinates becoming off.
/// </summary>
public class UniformTextureAtlas : GenericDataHolder {
/// <summary>
/// The texture to use for this atlas
/// </summary>
public readonly Texture2D Texture;
/// <summary>
/// The amount of sub-regions this atlas has in the x direction
/// </summary>
public readonly int RegionAmountX;
/// <summary>
/// The amount of sub-regions this atlas has in the y direction
/// </summary>
public readonly int RegionAmountY;
/// <summary>
/// The width of each region, based on the texture's width and the amount of regions
/// </summary>
public readonly int RegionWidth;
/// <summary>
/// The height of reach region, based on the texture's height and the amount of regions
/// </summary>
public readonly int RegionHeight;
/// <summary>
/// Returns the <see cref="TextureRegion"/> at this texture atlas's given index.
/// The index is zero-based, where rows come first and columns come second.
/// </summary>
/// <param name="index">The zero-based texture index</param>
public TextureRegion this[int index] => this[index % this.RegionAmountX, index / this.RegionAmountX];
/// <summary>
/// Returns the <see cref="TextureRegion"/> at this texture atlas' given region position
/// </summary>
/// <param name="point">The region's x and y location</param>
public TextureRegion this[Point point] => this[new Rectangle(point, new Point(1, 1))];
/// <inheritdoc cref="this[Point]"/>
public TextureRegion this[int x, int y] => this[new Point(x, y)];
/// <summary>
/// Returns the <see cref="TextureRegion"/> at this texture atlas' given region position and size.
/// Note that the region size is not in pixels, but in region units.
/// </summary>
/// <param name="rect">The region's area</param>
public TextureRegion this[Rectangle rect] => this.GetOrAddRegion(rect);
/// <inheritdoc cref="this[Rectangle]"/>
public TextureRegion this[int x, int y, int width, int height] => this[new Rectangle(x, y, width, height)];
private readonly Dictionary<Rectangle, TextureRegion> regions = new Dictionary<Rectangle, TextureRegion>();
/// <summary>
/// Creates a new uniform texture atlas with the given texture and region amount.
/// </summary>
/// <param name="texture">The texture to use for this atlas</param>
/// <param name="regionAmountX">The amount of texture regions in the x direction</param>
/// <param name="regionAmountY">The amount of texture regions in the y direction</param>
public UniformTextureAtlas(Texture2D texture, int regionAmountX, int regionAmountY) {
this.Texture = texture;
this.RegionAmountX = regionAmountX;