1
0
Fork 0
mirror of https://github.com/Ellpeck/MLEM.git synced 2024-11-22 20:58:34 +01:00

start on xml documentation, yay

This commit is contained in:
Ellpeck 2020-05-20 23:59:40 +02:00
parent b836b486a2
commit d8daa3779a
12 changed files with 439 additions and 9 deletions

View file

@ -5,10 +5,21 @@ using MLEM.Misc;
using MLEM.Textures;
namespace MLEM.Animations {
/// <summary>
/// A sprite animation that allows for any number of frames that each last any number of seconds
/// </summary>
public class SpriteAnimation : GenericDataHolder {
private AnimationFrame[] frames;
/// <summary>
/// Returns the <see cref="AnimationFrame"/> at the given index.
/// Index ordering is based on the order that animation frames were added in.
/// </summary>
/// <param name="index">The index in the list of animation frames</param>
public AnimationFrame this[int index] => this.frames[index];
/// <summary>
/// The frame that the animation is currently on.
/// </summary>
public AnimationFrame CurrentFrame {
get {
// we might have overshot the end time by a little bit, so just return the last frame
@ -24,31 +35,82 @@ namespace MLEM.Animations {
return this.frames[0];
}
}
/// <summary>
/// The texture region that the animation's <see cref="CurrentFrame"/> has
/// </summary>
public TextureRegion CurrentRegion => this.CurrentFrame.Region;
/// <summary>
/// The total amount of time that this animation has.
/// This is auatomatically calculated based on the frame time of each frame.
/// </summary>
public readonly double TotalTime;
/// <summary>
/// The amount of seconds that the animation has been going on for.
/// If <see cref="TotalTime"/> is reached, this value resets to 0.
/// </summary>
public double TimeIntoAnimation { get; private set; }
/// <summary>
/// The finished state of this animation.
/// This is only true for longer than a frame if <see cref="IsLooping"/> is false.
/// </summary>
public bool IsFinished { get; private set; }
/// <summary>
/// The name of this animation. This is useful if used in combination with <see cref="SpriteAnimationGroup"/>.
/// </summary>
public string Name;
/// <summary>
/// The speed multiplier that this animation should run with.
/// Numbers higher than 1 will increase the speed.
/// </summary>
public float SpeedMultiplier = 1;
/// <summary>
/// Set to false to stop this animation from looping.
/// To check if the animation has finished playing, see <see cref="IsFinished"/>.
/// </summary>
public bool IsLooping = true;
public Completed OnCompleted;
/// <summary>
/// A callback that gets fired when the animation completes.
/// </summary>
public event Completed OnCompleted;
/// <summary>
/// Set this to true to pause the playback of the animation.
/// <see cref="TimeIntoAnimation"/> will not continue and the <see cref="CurrentFrame"/> will not change.
/// </summary>
public bool IsPaused;
/// <summary>
/// Creates a new sprite animation that contains the given frames.
/// </summary>
/// <param name="frames">The frames this animation should have</param>
public SpriteAnimation(params AnimationFrame[] frames) {
this.frames = frames;
foreach (var frame in frames)
this.TotalTime += frame.Seconds;
}
/// <summary>
/// Creates a new sprite animation that contains the given texture regions as frames.
/// </summary>
/// <param name="timePerFrame">The amount of time that each frame should last for</param>
/// <param name="regions">The texture regions that should make up this animation</param>
public SpriteAnimation(double timePerFrame, params TextureRegion[] regions)
: this(Array.ConvertAll(regions, region => new AnimationFrame(region, timePerFrame))) {
}
/// <summary>
/// Creates a new sprite animation based on the given texture regions in rectangle-based format.
/// </summary>
/// <param name="timePerFrame">The amount of time that each frame should last for</param>
/// <param name="texture">The texture that the regions should come from</param>
/// <param name="regions">The texture regions that should make up this animation</param>
public SpriteAnimation(double timePerFrame, Texture2D texture, params Rectangle[] regions)
: this(timePerFrame, Array.ConvertAll(regions, region => new TextureRegion(texture, region))) {
}
/// <summary>
/// Updates this animation, causing <see cref="TimeIntoAnimation"/> to be increased and the <see cref="CurrentFrame"/> to be updated.
/// </summary>
/// <param name="time">The game's time</param>
public void Update(GameTime time) {
this.SetTime(this.TimeIntoAnimation + time.ElapsedGameTime.TotalSeconds * this.SpeedMultiplier);
}
@ -67,6 +129,9 @@ namespace MLEM.Animations {
}
}
/// <summary>
/// Restarts this animation from the first frame.
/// </summary>
public void Restart() {
this.TimeIntoAnimation = 0;
this.IsFinished = false;
@ -77,11 +142,25 @@ namespace MLEM.Animations {
}
/// <summary>
/// Represents a single frame of a <see cref="SpriteAnimation"/>
/// </summary>
public class AnimationFrame {
/// <summary>
/// The texture region that this frame should render
/// </summary>
public readonly TextureRegion Region;
/// <summary>
/// The total amount of seconds that this frame should last for
/// </summary>
public readonly double Seconds;
/// <summary>
/// Creates a new animation frame based on a texture region and a time
/// </summary>
/// <param name="region">The texture region that this frame should render</param>
/// <param name="seconds">The total amount of seconds that this frame should last for</param>
public AnimationFrame(TextureRegion region, double seconds) {
this.Region = region;
this.Seconds = seconds;

View file

@ -5,6 +5,10 @@ using MLEM.Misc;
using MLEM.Textures;
namespace MLEM.Animations {
/// <summary>
/// Represents a list of <see cref="SpriteAnimation"/> objects with a condition and priority attached to them.
/// Sprite animation groups can be used if any single entity should have multiple animations (like up, down, left, right standing and running animations) that should be automatically managed.
/// </summary>
public class SpriteAnimationGroup : GenericDataHolder {
private readonly List<ConditionedAnimation> animations = new List<ConditionedAnimation>();
@ -20,11 +24,24 @@ namespace MLEM.Animations {
}
set => this.currAnimation = value;
}
/// <summary>
/// The animation that is currently playing
/// </summary>
public SpriteAnimation CurrentAnimation => this.CurrAnimation?.Animation;
/// <summary>
/// The frame that <see cref="CurrentAnimation"/> is displaying
/// </summary>
public AnimationFrame CurrentFrame => this.CurrentAnimation?.CurrentFrame;
/// <summary>
/// The region that <see cref="CurrentFrame"/> has
/// </summary>
public TextureRegion CurrentRegion => this.CurrentAnimation?.CurrentRegion;
public AnimationChanged OnAnimationChanged;
/// <summary>
/// A callback for when the currently displaying animation has changed due to a condition with a higher priority being met.
/// </summary>
public event AnimationChanged OnAnimationChanged;
private bool isDirty;
/// <inheritdoc cref="SpriteAnimation.SpeedMultiplier"/>
public float SpeedMultiplier {
set {
foreach (var anim in this.animations)
@ -32,23 +49,36 @@ namespace MLEM.Animations {
}
}
/// <summary>
/// Adds a <see cref="SpriteAnimation"/> to this group.
/// </summary>
/// <param name="anim">The animation to add</param>
/// <param name="condition">The condition that needs to be met for this animation to play</param>
/// <param name="priority">The priority of this animation. The higher the priority, the earlier it is picked for playing.</param>
/// <returns>This group, for chaining</returns>
public SpriteAnimationGroup Add(SpriteAnimation anim, Func<bool> condition, int priority = 0) {
this.animations.Add(new ConditionedAnimation(anim, condition, priority));
this.isDirty = true;
return this;
}
/// <inheritdoc cref="SpriteAnimation.Update"/>
public void Update(GameTime time) {
this.FindAnimationToPlay();
if (this.CurrAnimation != null)
this.CurrAnimation.Animation.Update(time);
}
/// <summary>
/// Find an animation in this group by name and returns it.
/// </summary>
/// <param name="name">The <see cref="SpriteAnimation.Name"/> of the animation</param>
/// <returns>The animation by that name, or <c>null</c> if there is none</returns>
public SpriteAnimation ByName(string name) {
return this.animations.Find(anim => anim.Animation.Name == name)?.Animation;
}
public void FindAnimationToPlay() {
private void FindAnimationToPlay() {
ConditionedAnimation animToPlay = null;
if (this.CurrAnimation != null && this.CurrAnimation.ShouldPlay())
animToPlay = this.CurrAnimation;

View file

@ -5,18 +5,45 @@ using MLEM.Extensions;
using MLEM.Misc;
namespace MLEM.Cameras {
/// <summary>
/// Represents a simple, orthographic 2-dimensional camera that can be moved, scaled and that supports automatic viewport sizing.
/// To draw with the camera's positioning and scaling applied, use <see cref="ViewMatrix"/>.
/// </summary>
public class Camera {
/// <summary>
/// The top-left corner of the camera's viewport.
/// <seealso cref="LookingPosition"/>
/// </summary>
public Vector2 Position;
/// <summary>
/// The scale that this camera's <see cref="ViewMatrix"/> should have.
/// </summary>
public float Scale {
get => this.scale;
set => this.scale = MathHelper.Clamp(value, this.MinScale, this.MaxScale);
}
private float scale = 1;
/// <summary>
/// The minimum <see cref="Scale"/> that the camera can have
/// </summary>
public float MinScale = 0;
/// <summary>
/// The maximum <see cref="Scale"/> that the camera can have
/// </summary>
public float MaxScale = float.MaxValue;
/// <summary>
/// If this is true, the camera will automatically adapt to changed screen sizes.
/// You can use <see cref="AutoScaleReferenceSize"/> to determine the initial screen size that this camera should base its calculations on.
/// </summary>
public bool AutoScaleWithScreen;
/// <summary>
/// <seealso cref="AutoScaleWithScreen"/>
/// </summary>
public Point AutoScaleReferenceSize;
/// <summary>
/// The scale that this camera currently has, based on <see cref="Scale"/> and <see cref="AutoScaleReferenceSize"/> if <see cref="AutoScaleWithScreen"/> is true.
/// </summary>
public float ActualScale {
get {
if (!this.AutoScaleWithScreen)
@ -24,6 +51,10 @@ namespace MLEM.Cameras {
return Math.Min(this.Viewport.Width / (float) this.AutoScaleReferenceSize.X, this.Viewport.Height / (float) this.AutoScaleReferenceSize.Y) * this.Scale;
}
}
/// <summary>
/// The matrix that this camera "sees", based on its position and scale.
/// Use this in your <see cref="SpriteBatch.Begin"/> calls to render based on the camera's viewport.
/// </summary>
public Matrix ViewMatrix {
get {
var sc = this.ActualScale;
@ -33,39 +64,73 @@ namespace MLEM.Cameras {
return Matrix.CreateScale(sc, sc, 1) * Matrix.CreateTranslation(new Vector3(pos, 0));
}
}
/// <summary>
/// The bottom-right corner of the camera's viewport
/// <seealso cref="LookingPosition"/>
/// </summary>
public Vector2 Max {
get => this.Position + this.ScaledViewport;
set => this.Position = value - this.ScaledViewport;
}
/// <summary>
/// The center of the camera's viewport, or the position that the camera is looking at.
/// </summary>
public Vector2 LookingPosition {
get => this.Position + this.ScaledViewport / 2;
set => this.Position = value - this.ScaledViewport / 2;
}
public Rectangle Viewport => this.graphicsDevice.Viewport.Bounds;
public Vector2 ScaledViewport => new Vector2(this.Viewport.Width, this.Viewport.Height) / this.ActualScale;
private Rectangle Viewport => this.graphicsDevice.Viewport.Bounds;
private Vector2 ScaledViewport => new Vector2(this.Viewport.Width, this.Viewport.Height) / this.ActualScale;
private readonly bool roundPosition;
private readonly GraphicsDevice graphicsDevice;
/// <summary>
/// Creates a new camera.
/// </summary>
/// <param name="graphicsDevice">The game's graphics device</param>
/// <param name="roundPosition">If this is true, the camera's <see cref="Position"/> and related properties will be rounded to full integers.</param>
public Camera(GraphicsDevice graphicsDevice, bool roundPosition = true) {
this.graphicsDevice = graphicsDevice;
this.AutoScaleReferenceSize = this.Viewport.Size;
this.roundPosition = roundPosition;
}
/// <summary>
/// Converts a given position in screen space to world space
/// </summary>
/// <param name="pos">The position in screen space</param>
/// <returns>The position in world space</returns>
public Vector2 ToWorldPos(Vector2 pos) {
return Vector2.Transform(pos, Matrix.Invert(this.ViewMatrix));
}
/// <summary>
/// Converts a given position in world space to screen space
/// </summary>
/// <param name="pos">The position in world space</param>
/// <returns>The position in camera space</returns>
public Vector2 ToCameraPos(Vector2 pos) {
return Vector2.Transform(pos, this.ViewMatrix);
}
/// <summary>
/// Returns the area that this camera can see, in world space.
/// This can be useful for culling of tile and other entity renderers.
/// </summary>
/// <returns>A rectangle that represents the camera's visible area in world space</returns>
public RectangleF GetVisibleRectangle() {
var start = this.ToWorldPos(Vector2.Zero);
return new RectangleF(start, this.ToWorldPos(new Vector2(this.Viewport.Width, this.Viewport.Height)) - start);
}
/// <summary>
/// Forces the camera's bounds into the given min and max positions in world space.
/// If the space represented by the given values is smaller than what the camera can see, its position will be forced into the center of the area.
/// </summary>
/// <param name="min">The top left bound, in world space</param>
/// <param name="max">The bottom right bound, in world space</param>
/// <returns>Whether or not the camera's position changed as a result of the constraint</returns>
public bool ConstrainWorldBounds(Vector2 min, Vector2 max) {
var lastPos = this.Position;
var visible = this.GetVisibleRectangle();
@ -89,6 +154,11 @@ namespace MLEM.Cameras {
return !this.Position.Equals(lastPos, 0.001F);
}
/// <summary>
/// Zoom in the camera's view by a given amount, optionally focusing on a given center point.
/// </summary>
/// <param name="delta">The amount to zoom in or out by</param>
/// <param name="zoomCenter">The position that should be regarded as the zoom's center, in screen space. The default value is the center.</param>
public void Zoom(float delta, Vector2? zoomCenter = null) {
var center = (zoomCenter ?? this.Viewport.Size.ToVector2() / 2) / this.ActualScale;
var lastScale = this.Scale;

View file

@ -8,6 +8,9 @@ using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
namespace MLEM.Content {
/// <summary>
/// Represents a version of <see cref="ContentManager"/> that doesn't load content binary <c>xnb</c> files, but rather as their regular formats.
/// </summary>
public class RawContentManager : ContentManager, IGameComponent {
private static readonly RawContentReader[] Readers = AppDomain.CurrentDomain.GetAssemblies()
@ -18,14 +21,29 @@ namespace MLEM.Content {
private readonly List<IDisposable> disposableAssets = new List<IDisposable>();
/// <summary>
/// The graphics device that this content manager uses
/// </summary>
public readonly GraphicsDevice GraphicsDevice;
/// <summary>
/// Creates a new content manager with an optionally specified root directory.
/// </summary>
/// <param name="serviceProvider">The service provider of your game</param>
/// <param name="rootDirectory">The root directory. Defaults to "Content"</param>
public RawContentManager(IServiceProvider serviceProvider, string rootDirectory = "Content") :
base(serviceProvider, rootDirectory) {
if (serviceProvider.GetService(typeof(IGraphicsDeviceService)) is IGraphicsDeviceService s)
this.GraphicsDevice = s.GraphicsDevice;
}
/// <summary>
/// Loads a raw asset with the given name, based on the <see cref="ContentManager.RootDirectory"/>.
/// If the asset was previously loaded using this method, the cached asset is merely returned.
/// </summary>
/// <param name="assetName">The path and name of the asset to load, without extension.</param>
/// <typeparam name="T">The type of asset to load</typeparam>
/// <returns>The asset, either loaded from the cache, or from disk.</returns>
public override T Load<T>(string assetName) {
if (this.LoadedAssets.TryGetValue(assetName, out var ret) && ret is T t)
return t;

View file

@ -2,26 +2,59 @@ using System;
using System.IO;
namespace MLEM.Content {
/// <summary>
/// Represents a way for any kind of raw content file to be read using a <see cref="RawContentManager"/>
/// </summary>
public abstract class RawContentReader {
/// <summary>
/// Returns if the given type can be loaded by this content reader
/// </summary>
/// <param name="t">The type of asset</param>
/// <returns>If <see cref="t"/> can be loaded by this content reader</returns>
public abstract bool CanRead(Type t);
/// <summary>
/// Reads the content file from disk and returns it.
/// </summary>
/// <param name="manager">The <see cref="RawContentManager"/> that is loading the asset</param>
/// <param name="assetPath">The full path to the asset, starting from the <see cref="RawContentManager.RootDirectory"/></param>
/// <param name="stream">A stream that leads to this asset</param>
/// <param name="t">The type of asset to load</param>
/// <param name="existing">If this asset is being reloaded, this value contains the previous version of the asset.</param>
/// <returns>The loaded asset</returns>
public abstract object Read(RawContentManager manager, string assetPath, Stream stream, Type t, object existing);
/// <summary>
/// Represents the list of file extensions that this reader can read from.
/// </summary>
/// <returns>The list of valid extensions</returns>
public abstract string[] GetFileExtensions();
}
/// <inheritdoc/>
public abstract class RawContentReader<T> : RawContentReader {
/// <inheritdoc/>
public override bool CanRead(Type t) {
return typeof(T).IsAssignableFrom(t);
}
/// <inheritdoc/>
public override object Read(RawContentManager manager, string assetPath, Stream stream, Type t, object existing) {
return this.Read(manager, assetPath, stream, (T) existing);
}
/// <summary>
/// Reads the content file that is represented by our generic type from disk.
/// </summary>
/// <param name="manager">The <see cref="RawContentManager"/> that is loading the asset</param>
/// <param name="assetPath">The full path to the asset, starting from the <see cref="RawContentManager.RootDirectory"/></param>
/// <param name="stream">A stream that leads to this asset</param>
/// <param name="t">The type of asset to load</param>
/// <param name="existing">If this asset is being reloaded, this value contains the previous version of the asset.</param>
/// <returns>The loaded asset</returns>
protected abstract T Read(RawContentManager manager, string assetPath, Stream stream, T existing);
}

View file

@ -5,20 +5,43 @@ using Microsoft.Xna.Framework;
namespace MLEM.Extensions {
public static class ColorExtensions {
/// <summary>
/// Returns an inverted version of the color.
/// </summary>
/// <param name="color">The color to invert</param>
/// <returns>The inverted color</returns>
public static Color Invert(this Color color) {
return new Color(Math.Abs(255 - color.R), Math.Abs(255 - color.G), Math.Abs(255 - color.B), color.A);
}
/// <summary>
/// Parses a hexadecimal number into a color.
/// The number should be in the format <c>0xaarrggbb</c>.
/// </summary>
/// <param name="value">The number to parse</param>
/// <returns>The resulting color</returns>
public static Color FromHex(uint value) {
return new Color((int) (value >> 16 & 0xFF), (int) (value >> 8 & 0xFF), (int) (value >> 0 & 0xFF), (int) (value >> 24 & 0xFF));
}
/// <summary>
/// Parses a hexadecimal string into a color.
/// The string can optionally start with a <c>#</c>.
/// </summary>
/// <param name="value">The string to parse</param>
/// <returns>The resulting color</returns>
public static Color FromHex(string value) {
if (value.StartsWith("#"))
value = value.Substring(1);
return FromHex(uint.Parse(value, NumberStyles.HexNumber));
}
/// <summary>
/// Copies the alpha value from <see cref="other"/> 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>
public static Color CopyAlpha(this Color color, Color other) {
return color * (other.A / 255F);
}

View file

@ -9,6 +9,12 @@ namespace MLEM.Extensions {
private static int lastWidth;
private static int lastHeight;
/// <summary>
/// Sets the graphics device manager to fullscreen, properly taking into account the preferred backbuffer width and height to avoid lower resolutions for higher resolution screens.
/// </summary>
/// <param name="manager">The graphics device manager</param>
/// <param name="fullscreen">True if fullscreen should be enabled, false if disabled</param>
/// <exception cref="InvalidOperationException">Thrown when changing out of fullscreen mode before changing into fullscreen mode using this method</exception>
public static void SetFullscreen(this GraphicsDeviceManager manager, bool fullscreen) {
manager.IsFullScreen = fullscreen;
if (fullscreen) {
@ -29,6 +35,10 @@ namespace MLEM.Extensions {
manager.ApplyChanges();
}
/// <summary>
/// Save version of <see cref="GraphicsDeviceManager.ApplyChanges"/> that doesn't reset window size to defaults
/// </summary>
/// <param name="manager">The graphics device manager</param>
public static void ApplyChangesSafely(this GraphicsDeviceManager manager) {
// If we don't do this, then applying changes will cause the
// graphics device manager to reset the window size to the
@ -39,6 +49,11 @@ namespace MLEM.Extensions {
manager.ApplyChanges();
}
/// <summary>
/// Resets preferred width and height back to the window's default bound values.
/// </summary>
/// <param name="manager">The graphics device manager</param>
/// <param name="window">The window whose bounds to use</param>
public static void ResetWidthAndHeight(this GraphicsDeviceManager manager, GameWindow window) {
var (_, _, width, height) = window.ClientBounds;
manager.PreferredBackBufferWidth = Math.Max(height, width);
@ -46,6 +61,13 @@ namespace MLEM.Extensions {
manager.ApplyChanges();
}
/// <summary>
/// Starts a new <see cref="TargetContext"/> using the specified render target.
/// The returned context automatically disposes when used in a <c>using</c> statement, which causes any previously applied render targets to be reapplied automatically.
/// </summary>
/// <param name="device">The graphics device</param>
/// <param name="target">The render target to apply</param>
/// <returns></returns>
public static TargetContext WithRenderTarget(this GraphicsDevice device, RenderTarget2D target) {
return new TargetContext(device, target);
}

View file

@ -5,68 +5,115 @@ using MLEM.Misc;
namespace MLEM.Extensions {
public static class NumberExtensions {
/// <inheritdoc cref="Math.Floor(decimal)"/>
public static int Floor(this float f) {
return (int) Math.Floor(f);
}
/// <inheritdoc cref="Math.Ceiling(decimal)"/>
public static int Ceil(this float f) {
return (int) Math.Ceiling(f);
}
/// <summary>
/// Checks for decimal equality with a given tolerance.
/// </summary>
/// <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>
public static bool Equals(this float first, float second, float tolerance) {
return Math.Abs(first - second) <= tolerance;
}
/// <inheritdoc cref="Equals(float,float,float)"/>
public static bool Equals(this Vector2 first, Vector2 second, float tolerance) {
return Math.Abs(first.X - second.X) <= tolerance && Math.Abs(first.Y - second.Y) <= tolerance;
}
/// <inheritdoc cref="Equals(float,float,float)"/>
public static bool Equals(this Vector3 first, Vector3 second, float tolerance) {
return Math.Abs(first.X - second.X) <= tolerance && Math.Abs(first.Y - second.Y) <= tolerance && Math.Abs(first.Z - second.Z) <= tolerance;
}
/// <inheritdoc cref="Equals(float,float,float)"/>
public static bool Equals(this Vector4 first, Vector4 second, float tolerance) {
return Math.Abs(first.X - second.X) <= tolerance && Math.Abs(first.Y - second.Y) <= tolerance && Math.Abs(first.Z - second.Z) <= tolerance && Math.Abs(first.W - second.W) <= tolerance;
}
/// <inheritdoc cref="Math.Floor(decimal)"/>
public static Vector2 Floor(this Vector2 vec) {
return new Vector2(vec.X.Floor(), vec.Y.Floor());
}
/// <inheritdoc cref="Math.Floor(decimal)"/>
public static Vector3 Floor(this Vector3 vec) {
return new Vector3(vec.X.Floor(), vec.Y.Floor(), vec.Z.Floor());
}
/// <inheritdoc cref="Math.Floor(decimal)"/>
public static Vector4 Floor(this Vector4 vec) {
return new Vector4(vec.X.Floor(), vec.Y.Floor(), vec.Z.Floor(), vec.W.Floor());
}
/// <summary>
/// Multiplies a point by a given scalar.
/// </summary>
/// <param name="point">The point</param>
/// <param name="f">The scalar</param>
/// <returns>The point, multiplied by the scalar memberwise</returns>
public static Point Multiply(this Point point, float f) {
return new Point((point.X * f).Floor(), (point.Y * f).Floor());
}
/// <summary>
/// Divides a point by a given scalar.
/// </summary>
/// <param name="point">The point</param>
/// <param name="f">The scalar</param>
/// <returns>The point, divided by the scalar memberwise</returns>
public static Point Divide(this Point point, float f) {
return new Point((point.X / f).Floor(), (point.Y / f).Floor());
}
/// <summary>
/// Transforms a point by a given matrix.
/// </summary>
/// <param name="position">The point</param>
/// <param name="matrix">The matrix</param>
/// <returns>The point, transformed by the matrix</returns>
public static Point Transform(this Point position, Matrix matrix) {
return new Point(
(position.X * matrix.M11 + position.Y * matrix.M21 + matrix.M41).Floor(),
(position.X * matrix.M12 + position.Y * matrix.M22 + matrix.M42).Floor());
}
/// <summary>
/// Returns a copy of the given rectangle, moved by the given point.
/// The rectangle's size remains unchanged.
/// </summary>
/// <param name="rect">The rectangle to move</param>
/// <param name="offset">The amount to move by</param>
/// <returns>The moved rectangle</returns>
public static Rectangle OffsetCopy(this Rectangle rect, Point offset) {
rect.X += offset.X;
rect.Y += offset.Y;
return rect;
}
/// <inheritdoc cref="OffsetCopy(Microsoft.Xna.Framework.Rectangle,Microsoft.Xna.Framework.Point)"/>
public static RectangleF OffsetCopy(this RectangleF rect, Vector2 offset) {
rect.X += offset.X;
rect.Y += offset.Y;
return rect;
}
/// <summary>
/// Shrinks the rectangle by the given padding, causing its size to decrease by twice the amount and its position to be moved inwards by the amount.
/// </summary>
/// <param name="rect">The rectangle to shrink</param>
/// <param name="padding">The padding to shrink by</param>
/// <returns>The shrunk rectangle</returns>
public static Rectangle Shrink(this Rectangle rect, Point padding) {
rect.X += padding.X;
rect.Y += padding.Y;
@ -75,6 +122,7 @@ namespace MLEM.Extensions {
return rect;
}
/// <inheritdoc cref="Shrink(Microsoft.Xna.Framework.Rectangle,Microsoft.Xna.Framework.Point)"/>
public static RectangleF Shrink(this RectangleF rect, Vector2 padding) {
rect.X += padding.X;
rect.Y += padding.Y;
@ -83,6 +131,7 @@ namespace MLEM.Extensions {
return rect;
}
/// <inheritdoc cref="Shrink(Microsoft.Xna.Framework.Rectangle,Microsoft.Xna.Framework.Point)"/>
public static RectangleF Shrink(this RectangleF rect, Padding padding) {
rect.X += padding.Left;
rect.Y += padding.Top;

View file

@ -5,14 +5,32 @@ using System.Linq;
namespace MLEM.Extensions {
public static class RandomExtensions {
/// <summary>
/// Gets a random entry from the given list with uniform chance.
/// </summary>
/// <param name="random">The random</param>
/// <param name="entries">The entries to choose from</param>
/// <typeparam name="T">The entries' type</typeparam>
/// <returns>A random entry</returns>
public static T GetRandomEntry<T>(this Random random, params T[] entries) {
return entries[random.Next(entries.Length)];
}
/// <inheritdoc cref="GetRandomEntry{T}(System.Random,T[])"/>
public static T GetRandomEntry<T>(this Random random, IList<T> entries) {
return entries[random.Next(entries.Count)];
}
/// <summary>
/// Returns a random entry from the given list based on the specified weight function.
/// A higher weight for an entry increases its likeliness of being picked.
/// </summary>
/// <param name="random">The random</param>
/// <param name="entries">The entries to choose from</param>
/// <param name="weightFunc">A function that applies weight to each entry</param>
/// <typeparam name="T">The entries' type</typeparam>
/// <returns>A random entry, based on the entries' weight</returns>
/// <exception cref="IndexOutOfRangeException">If the weight function returns different weights for the same entry</exception>
public static T GetRandomWeightedEntry<T>(this Random random, IList<T> entries, Func<T, int> weightFunc) {
var totalWeight = entries.Sum(weightFunc);
var goalWeight = random.Next(totalWeight);

View file

@ -8,6 +8,12 @@ namespace MLEM.Extensions {
private static Texture2D blankTexture;
/// <summary>
/// Returns a 1x1 pixel white texture that can be used for drawing solid color shapes.
/// This texture is automatically disposed of when the batch is disposed.
/// </summary>
/// <param name="batch">The sprite batch</param>
/// <returns>A 1x1 pixel white texture</returns>
public static Texture2D GetBlankTexture(this SpriteBatch batch) {
if (blankTexture == null) {
blankTexture = new Texture2D(batch.GraphicsDevice, 1, 1);
@ -22,6 +28,13 @@ namespace MLEM.Extensions {
return blankTexture;
}
/// <summary>
/// Generates a <see cref="NinePatch"/> that has a texture with a given color and outline color
/// </summary>
/// <param name="batch">The sprite batch</param>
/// <param name="color">The fill color of the texture</param>
/// <param name="outlineColor">The outline color of the texture</param>
/// <returns>A <see cref="NinePatch"/> containing a 3x3 texture with an outline</returns>
public static NinePatch GenerateTexture(this SpriteBatch batch, Color color, Color? outlineColor = null) {
var outli = outlineColor ?? Color.Black;
var tex = new Texture2D(batch.GraphicsDevice, 3, 3);
@ -39,16 +52,19 @@ namespace MLEM.Extensions {
return new NinePatch(tex, 1);
}
/// <inheritdoc cref="SpriteBatch.Draw(Texture2D,Rectangle,Rectangle?,Color,float,Vector2,SpriteEffects,float)"/>
public static void Draw(this SpriteBatch batch, Texture2D texture, RectangleF destinationRectangle, Rectangle? sourceRectangle, Color color, float rotation, Vector2 origin, SpriteEffects effects, float layerDepth) {
var source = sourceRectangle ?? new Rectangle(0, 0, texture.Width, texture.Height);
var scale = new Vector2(1F / source.Width, 1F / source.Height) * destinationRectangle.Size;
batch.Draw(texture, destinationRectangle.Location, sourceRectangle, color, rotation, origin, scale, effects, layerDepth);
}
/// <inheritdoc cref="SpriteBatch.Draw(Texture2D,Rectangle,Rectangle?,Color)"/>
public static void Draw(this SpriteBatch batch, Texture2D texture, RectangleF destinationRectangle, Rectangle? sourceRectangle, Color color) {
batch.Draw(texture, destinationRectangle, sourceRectangle, color, 0, Vector2.Zero, SpriteEffects.None, 0);
}
/// <inheritdoc cref="SpriteBatch.Draw(Texture2D,Rectangle,Color)"/>
public static void Draw(this SpriteBatch batch, Texture2D texture, RectangleF destinationRectangle, Color color) {
batch.Draw(texture, destinationRectangle, null, color);
}

View file

@ -5,38 +5,67 @@ using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace MLEM.Font {
/// <summary>
/// Represents a font with additional abilities.
/// <seealso cref="GenericSpriteFont"/>
/// </summary>
public abstract class GenericFont {
/// <summary>
/// The bold version of this font.
/// </summary>
public abstract GenericFont Bold { get; }
/// <summary>
/// The italic version of this font.
/// </summary>
public abstract GenericFont Italic { get; }
///<inheritdoc cref="SpriteFont.LineSpacing"/>
public abstract float LineHeight { get; }
///<inheritdoc cref="SpriteFont.MeasureString(string)"/>
public abstract Vector2 MeasureString(string text);
///<inheritdoc cref="SpriteFont.MeasureString(StringBuilder)"/>
public abstract Vector2 MeasureString(StringBuilder text);
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
public abstract void DrawString(SpriteBatch batch, string text, Vector2 position, Color color);
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
public abstract void DrawString(SpriteBatch batch, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects effects, float layerDepth);
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
public abstract void DrawString(SpriteBatch batch, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth);
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
public abstract void DrawString(SpriteBatch batch, StringBuilder text, Vector2 position, Color color);
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
public abstract void DrawString(SpriteBatch batch, StringBuilder text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects effects, float layerDepth);
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
public abstract void DrawString(SpriteBatch batch, StringBuilder text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth);
/// <summary>
/// Draws a string with the given text alignment.
/// </summary>
/// <param name="batch">The sprite batch to use</param>
/// <param name="text">The string to draw</param>
/// <param name="position">The position of the top left corner of the string</param>
/// <param name="align">The alignment to use</param>
/// <param name="color">The color to use</param>
public void DrawString(SpriteBatch batch, string text, Vector2 position, TextAlign align, Color color) {
this.DrawString(batch, text, position, align, color, 0, Vector2.Zero, Vector2.One, SpriteEffects.None, 0);
}
///<inheritdoc cref="DrawString(SpriteBatch,string,Vector2,TextAlign,Color)"/>
public void DrawString(SpriteBatch batch, string text, Vector2 position, TextAlign align, Color color, float rotation, Vector2 origin, float scale, SpriteEffects effects, float layerDepth) {
this.DrawString(batch, text, position, align, color, rotation, origin, new Vector2(scale), effects, layerDepth);
}
///<inheritdoc cref="DrawString(SpriteBatch,string,Vector2,TextAlign,Color)"/>
public void DrawString(SpriteBatch batch, string text, Vector2 position, TextAlign align, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth) {
switch (align) {
case TextAlign.Center:
@ -53,6 +82,16 @@ namespace MLEM.Font {
this.DrawString(batch, text, position, color, rotation, origin, scale, effects, layerDepth);
}
/// <summary>
/// Truncates a string to a given width. If the string's displayed area is larger than the maximum width, the string is cut off.
/// Optionally, the string can be cut off a bit sooner, adding the <see cref="ellipsis"/> at the end instead.
/// </summary>
/// <param name="text">The text to truncate</param>
/// <param name="width">The maximum width, in display pixels based on the font and scale</param>
/// <param name="scale">The scale to use for width measurements</param>
/// <param name="fromBack">If the string should be truncated from the back rather than the front</param>
/// <param name="ellipsis">The characters to add to the end of the string if it is too long</param>
/// <returns>The truncated string, or the same string if it is shorter than the maximum width</returns>
public string TruncateString(string text, float width, float scale, bool fromBack = false, string ellipsis = "") {
var total = new StringBuilder();
var ellipsisWidth = this.MeasureString(ellipsis).X * scale;
@ -74,6 +113,14 @@ namespace MLEM.Font {
return total.ToString();
}
/// <summary>
/// Splits a string to a given maximum width, adding newline characters between each line.
/// Also splits long words.
/// </summary>
/// <param name="text">The text to split into multiple lines</param>
/// <param name="width">The maximum width that each line should have</param>
/// <param name="scale">The scale to use for width measurements</param>
/// <returns>The split string, containing newline characters at each new line</returns>
public string SplitString(string text, float width, float scale) {
var total = new StringBuilder();
foreach (var line in text.Split('\n')) {
@ -109,6 +156,12 @@ namespace MLEM.Font {
return total.ToString(0, total.Length - 2);
}
/// <summary>
/// Returns a string made up of the given content characters that is the given length long when displayed.
/// </summary>
/// <param name="width">The width that the string should have if the scale is 1</param>
/// <param name="content">The content that the string should contain. Defaults to a space.</param>
/// <returns></returns>
public string GetWidthString(float width, char content = ' ') {
var strg = content.ToString();
while (this.MeasureString(strg).X < width)

View file

@ -5,47 +5,66 @@ using Microsoft.Xna.Framework.Graphics;
using MLEM.Extensions;
namespace MLEM.Font {
/// <inheritdoc/>
public class GenericSpriteFont : GenericFont {
public readonly SpriteFont Font;
/// <inheritdoc/>
public override GenericFont Bold { get; }
/// <inheritdoc/>
public override GenericFont Italic { get; }
/// <inheritdoc/>
public override float LineHeight => this.Font.LineSpacing;
/// <summary>
/// Creates a new generic font using <see cref="SpriteFont"/>.
/// Optionally, a bold and italic version of the font can be supplied.
/// </summary>
/// <param name="font">The font to wrap</param>
/// <param name="bold">A bold version of the font</param>
/// <param name="italic">An italic version of the font</param>
public GenericSpriteFont(SpriteFont font, SpriteFont bold = null, SpriteFont italic = null) {
this.Font = font;
this.Bold = bold != null ? new GenericSpriteFont(bold) : this;
this.Italic = italic != null ? new GenericSpriteFont(italic) : this;
}
/// <inheritdoc/>
public override Vector2 MeasureString(string text) {
return this.Font.MeasureString(text);
}
/// <inheritdoc/>
public override Vector2 MeasureString(StringBuilder text) {
return this.Font.MeasureString(text);
}
/// <inheritdoc/>
public override void DrawString(SpriteBatch batch, string text, Vector2 position, Color color) {
batch.DrawString(this.Font, text, position, color);
}
/// <inheritdoc/>
public override void DrawString(SpriteBatch batch, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects effects, float layerDepth) {
batch.DrawString(this.Font, text, position, color, rotation, origin, scale, effects, layerDepth);
}
/// <inheritdoc/>
public override void DrawString(SpriteBatch batch, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth) {
batch.DrawString(this.Font, text, position, color, rotation, origin, scale, effects, layerDepth);
}
/// <inheritdoc/>
public override void DrawString(SpriteBatch batch, StringBuilder text, Vector2 position, Color color) {
batch.DrawString(this.Font, text, position, color);
}
/// <inheritdoc/>
public override void DrawString(SpriteBatch batch, StringBuilder text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects effects, float layerDepth) {
batch.DrawString(this.Font, text, position, color, rotation, origin, scale, effects, layerDepth);
}
/// <inheritdoc/>
public override void DrawString(SpriteBatch batch, StringBuilder text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth) {
batch.DrawString(this.Font, text, position, color, rotation, origin, scale, effects, layerDepth);
}