StarSim

StarSim.git
git clone git://git.lenczewski.org/StarSim.git
Log | Files | Refs | README | LICENSE

commit 2ee19c415092be8874b68caa5d720bdab161b609
parent f76395b726bb4a1e193bebca6a9169d43b25af24
Author: Mikolaj Lenczewski <33129490+EnderRifter@users.noreply.github.com>
Date:   Sun, 14 Jul 2019 22:21:19 +0100

Encapsulated simulation screen in its own class, so that it can be dealt with independently. Also renamed Drawer and InputHandler for ease of use.

Diffstat:
MStarSim/StarSim/Program.cs | 90+++++++------------------------------------------------------------------------
DStarSim/StarSimLib/Graphics/Drawer.cs | 487-------------------------------------------------------------------------------
AStarSim/StarSimLib/Graphics/SimulationDrawer.cs | 487+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MStarSim/StarSimLib/StarSimLib.xml | 162+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
DStarSim/StarSimLib/UI/InputHandler.cs | 212-------------------------------------------------------------------------------
AStarSim/StarSimLib/UI/SimulationInputHandler.cs | 212+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
AStarSim/StarSimLib/UI/SimulationScreen.cs | 143+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 963 insertions(+), 830 deletions(-)

diff --git a/StarSim/StarSim/Program.cs b/StarSim/StarSim/Program.cs @@ -24,26 +24,11 @@ namespace StarSim internal class Program { /// <summary> - /// The interval between timer refreshes, in milliseconds. - /// </summary> - private const double TimerRefreshIntervalMs = 500; - - /// <summary> /// Holds all the <see cref="Body"/> instances that should be simulated. /// </summary> private static readonly Body[] bodies; /// <summary> - /// The renderer used to display the <see cref="Body"/> instances on the screen. - /// </summary> - private static readonly Drawer bodyDrawer; - - /// <summary> - /// The body position update algorithm to use. - /// </summary> - private static readonly UpdateDelegate bodyPositionUpdater; - - /// <summary> /// Maps a <see cref="Body"/> to the <see cref="CircleShape"/> that represents it, and is drawn to the /// screen at the <see cref="Body"/>s position. /// </summary> @@ -55,14 +40,9 @@ namespace StarSim private static readonly SimulatorContext databaseContext; /// <summary> - /// The input handler to use to provide interactivity to the simulator. - /// </summary> - private static readonly InputHandler inputHandler; - - /// <summary> - /// Timer that manages FPS counter and other miscellaneous counters. + /// The simulation which we will render, once the user sets it up. /// </summary> - private static readonly Timer miscTimer; + private static readonly SimulationScreen simulationScreen; /// <summary> /// The SFML.NET window to which everything is rendered. @@ -70,16 +50,6 @@ namespace StarSim private static readonly RenderWindow window; /// <summary> - /// The current amount of frames per second. - /// </summary> - private static double fps; - - /// <summary> - /// Counts the frames elapsed since the last timer pulse, so that the FPS can be tracked. - /// </summary> - private static uint framesElapsed; - - /// <summary> /// Initialises a new instance of the <see cref="Program"/> class, /// </summary> static Program() @@ -95,23 +65,12 @@ namespace StarSim bodyShapeMap = BodyGenerator.GenerateShapes(bodies); #if DEBUG - bodyPositionUpdater = BodyUpdater.UpdateBodiesBruteForce; + UpdateDelegate bodyPositionUpdater = BodyUpdater.UpdateBodiesBruteForce; #else - bodyPositionUpdater = BodyUpdater.UpdateBodiesBarnesHut; + UpdateDelegate bodyPositionUpdater = BodyUpdater.UpdateBodiesBarnesHut; #endif - bodyDrawer = new Drawer(window, ref bodies, ref bodyShapeMap); - inputHandler = new InputHandler(ref bodies, ref bodyDrawer); - - // constructs a new timer and attaches a timer event handler that updates the fps and window title every interval - miscTimer = new Timer(TimerRefreshIntervalMs) { AutoReset = true, Enabled = true }; - miscTimer.Elapsed += (sender, args) => - { - fps = framesElapsed / (TimerRefreshIntervalMs / 1000); - - framesElapsed = 0; - window.SetTitle($"N-Body Simulator: FPS {fps}"); - }; + simulationScreen = new SimulationScreen(ref bodies, ref bodyShapeMap, bodyPositionUpdater); } /// <summary> @@ -148,46 +107,11 @@ namespace StarSim { Console.WriteLine("Hello, World!"); Console.WriteLine("Press 'enter' to continue..."); - Console.ReadLine(); - // we reveal the window so that the user may interact with our program - window.SetVisible(true); - miscTimer.Start(); - - // we configure the window and its close handler, so that we may close the window once we are done with it - window.SetFramerateLimit(Constants.FrameRate); - window.Closed += (sender, eventArgs) => ((RenderWindow)sender).Close(); - - // we apply event handlers to allow for interactivity inside the window - window.KeyPressed += inputHandler.HandleKeyPressed; - //window.KeyReleased += inputHandler.HandleKeyReleased; - window.MouseButtonPressed += inputHandler.HandleMousePressed; - window.MouseButtonReleased += inputHandler.HandleMouseReleased; - //window.MouseMoved += inputHandler.HandleMouseMoved; - window.MouseWheelScrolled += inputHandler.HandleMouseScrolled; - - bodyDrawer.DrawBodies(); - - while (window.IsOpen) - { - window.Clear(); - window.DispatchEvents(); - - if (!inputHandler.IsSimulationPaused) - { - bodyPositionUpdater(bodies, Constants.TimeStep); - } - - bodyDrawer.DrawBodies(); - - window.Display(); - - // increment the fps counter - framesElapsed++; - } + // run simulation + simulationScreen.Run(); - miscTimer.Stop(); Console.WriteLine("Goodbye, World!"); Console.WriteLine("Press 'enter' to quit..."); Console.ReadLine(); diff --git a/StarSim/StarSimLib/Graphics/Drawer.cs b/StarSim/StarSimLib/Graphics/Drawer.cs @@ -1,486 +0,0 @@ -using System; -using System.Collections.Generic; -using SFML.Graphics; -using SFML.System; -using StarSimLib.Data_Structures; - -namespace StarSimLib.Graphics -{ - /// <summary> - /// Enumerates all possible rotation directions (i.e. north, east, south, west, clockwise, anticlockwise). - /// Here, camera forward is north. - /// </summary> - public enum RotationDirection - { - /// <summary> - /// Represents an anticlockwise rotation in the x-axis. - /// </summary> - North, - - /// <summary> - /// Represents an anticlockwise rotation in the y-axis. - /// </summary> - East, - - /// <summary> - /// Represents a clockwise rotation in the x-axis. - /// </summary> - South, - - /// <summary> - /// Represents a clockwise rotation in the y-axis. - /// </summary> - West, - - /// <summary> - /// Represents a clockwise rotation in the z-axis. - /// </summary> - Clockwise, - - /// <summary> - /// Represents an anticlockwise rotation in the z-axis. - /// </summary> - Anticlockwise - } - - /// <summary> - /// Represents a drawer capable of rendering bodies to a <see cref="RenderTarget"/>. - /// </summary> - public class Drawer - { - /// <summary> - /// Conversion from degrees to radians, as used by the <see cref="Math"/> functions. - /// </summary> - private const double DegToRad = Math.PI / 180; - - /// <summary> - /// The furthest distance that can be seen on the <see cref="RenderTarget"/>. Any bodies that are further - /// from the camera than this distance are culled and not rendered. - /// </summary> - private const double FarDistance = Constants.UniverseSize; - - /// <summary> - /// The default field of view for the drawer in degrees. - /// </summary> - private const double FieldOfView = 45; - - /// <summary> - /// The closest distance that can be seen on the <see cref="RenderTarget"/>. Any bodies that are closer - /// to the camera than this distance (i.e. behind the camera) are culled and not rendered. - /// </summary> - private const double NearDistance = 0; - - /// <summary> - /// The vector by which all projected points are translated away from the camera and halfway into the view frustum. - /// </summary> - private static readonly Vector4 cameraTranslationVector = new Vector4(0, 0, (FarDistance - NearDistance) / 2); - - /// <summary> - /// The aspect ratio of the render target. Allows for normalisation of the <see cref="RenderTarget"/> space - /// into a normal plane ([-1, -1] = top left, [1, 1] = bottom right), instead of having to deal with a - /// variable render space. - /// </summary> - private readonly double aspectRatio; - - /// <summary> - /// Holds all the <see cref="Body"/> instances that should be simulated. - /// </summary> - private readonly Body[] managedBodies; - - /// <summary> - /// Maps a <see cref="Body"/> to the <see cref="CircleShape"/> that represents it, and is drawn to the - /// screen at the <see cref="Body"/>s position. - /// </summary> - private readonly Dictionary<Body, CircleShape> managedBodyShapeMap; - - /// <summary> - /// Maps a <see cref="Body"/> to the <see cref="VertexArray"/> that holds its orbit tracer vertices, and - /// is drawn to the screen behind the <see cref="Body"/> instance. - /// </summary> - private readonly Dictionary<Body, VertexArray> managedBodyTracerVertexArrayMap; - - /// <summary> - /// The offset that has to be applied to the positions of a <see cref="CircleShape"/>, so that they appear - /// to be in the centre of the render target and not the top left corner. - /// </summary> - private readonly Vector2u originOffset; - - /// <summary> - /// The target to which the managed bodies will be drawn. - /// </summary> - private readonly RenderTarget renderTarget; - - /// <summary> - /// The current value for the field of view, in degrees. Used to zoom the view in and out. - /// </summary> - private double currentFieldOfView = FieldOfView; - -#pragma warning disable IDE0044 // Set field to readonly - - /// <summary> - /// Projection matrix used for mapping from 3D to 2D. - /// </summary> - private Matrix4x4 projectionMatrix; - - /// <summary> - /// The 3D rotation. - /// </summary> - private EulerAngle rotation; - - /// <summary> - /// The rotation matrix for the x axis. - /// </summary> - private Matrix4x4 xRotationMatrix; - - /// <summary> - /// The rotation matrix for the y axis. - /// </summary> - private Matrix4x4 yRotationMatrix; - - /// <summary> - /// The rotation matrix for the z axis. - /// </summary> - private Matrix4x4 zRotationMatrix; - -#pragma warning restore IDE0044 - - /// <summary> - /// Initialises a new instance of the <see cref="Drawer"/> class. - /// </summary> - /// <param name="target">The target to which the managed bodies should be rendered.</param> - /// <param name="bodies"> - /// A reference to the <see cref="Body"/> instances which should be managed by this instance. - /// </param> - /// <param name="bodyShapeMap"> - /// A reference to the dictionary mapping a <see cref="Body"/> instance to the <see cref="CircleShape"/> that - /// represents it, and is drawn to the screen at the <see cref="Body"/> instance's position. - /// </param> - public Drawer(RenderTarget target, ref Body[] bodies, ref Dictionary<Body, CircleShape> bodyShapeMap) - { - renderTarget = target; - originOffset = target.Size / 2; - - aspectRatio = renderTarget.Size.Y / (double)renderTarget.Size.X; - - // projection and rotation matrices - projectionMatrix = new Matrix4x4(new[] - { - new [] { aspectRatio * InverseScaleFactor, 0, 0, 0 }, - new [] { 0, InverseScaleFactor, 0, 0 }, - new [] { 0, 0, FarDistance / (FarDistance - NearDistance), 1 }, - new [] { 0, 0, -FarDistance * NearDistance / (FarDistance - NearDistance), 0 } - }); - - xRotationMatrix = new Matrix4x4(new[] - { - new [] { 1d, 0, 0, 0 }, - new [] { 0d, 0, 0, 0 }, - new [] { 0d, 0, 0, 0 }, - new [] { 0d, 0, 0, 1 } - }); - - yRotationMatrix = new Matrix4x4(new[] - { - new [] { 0d, 0, 0, 0 }, - new [] { 0d, 1, 0, 0 }, - new [] { 0d, 0, 0, 0 }, - new [] { 0d, 0, 0, 1 } - }); - - zRotationMatrix = new Matrix4x4(new[] - { - new [] { 0d, 0, 0, 0 }, - new [] { 0d, 0, 0, 0 }, - new [] { 0d, 0, 1, 0 }, - new [] { 0d, 0, 0, 1 } - }); - - // set initial values for the rotation matrices - UpdateRotationMatrices(); - - managedBodies = bodies; - managedBodyShapeMap = bodyShapeMap; - managedBodyTracerVertexArrayMap = new Dictionary<Body, VertexArray>(); - - foreach (Body body in bodies) - { - // create a vertex array to store orbit tracer points for this body - managedBodyTracerVertexArrayMap.Add(body, - new VertexArray(PrimitiveType.LineStrip, Constants.StoredPreviousPositionCount)); - } - } - - /// <summary> - /// Inverse scale factor used in the projection matrix. - /// </summary> - private double InverseScaleFactor { get { return 1 / Math.Tan(currentFieldOfView * 0.5 * DegToRad); } } - - /// <summary> - /// Current field-of-view. - /// </summary> - public double FOV - { - get - { - return currentFieldOfView; - } - } - - /// <summary> - /// Current angle of rotation in the x axis. - /// </summary> - public double XAngle - { - get { return rotation.X; } - } - - /// <summary> - /// Current angle of rotation in the x axis. - /// </summary> - public double YAngle - { - get { return rotation.Y; } - } - - /// <summary> - /// Current angle of rotation in the x axis. - /// </summary> - public double ZAngle - { - get { return rotation.Z; } - } - - /// <summary> - /// Current zoom level of the drawer. - /// </summary> - public double ZoomLevel - { - get { return FieldOfView / currentFieldOfView; } - } - - /// <summary> - /// Linearly interpolates between a and b by the given percentage dt (0 = 100% a, 1 = 100% b). - /// </summary> - /// <param name="a">One of the starting values between which to interpolate.</param> - /// <param name="b">One of the starting values between which to interpolate.</param> - /// <param name="dt">The percentage by which to interpolate, capped between 0 and 1.</param> - /// <returns>The interpolated value.</returns> - private static Vector4 LinearInterpolate(Vector4 a, Vector4 b, double dt) - { - // caps the percentage first between 1 and -Infinity, and then between 1 and 0, and reverses it such - // that 0 is 100% a and 0% b, and that 1 is 0% a and 100% b. - dt = 1 - dt; - dt = dt > 1 ? 1 : dt; - dt = dt < 0 ? 0 : dt; - - /* formula: C = A + dt(B - A) / distance */ - Vector4 c = a + dt * (b - a) / 1; - - return c; - } - - /// <summary> - /// Projects the given <see cref="Vector4"/> point from world space into screen space. - /// </summary> - /// <param name="point">The point to project.</param> - /// <returns>The projected point.</returns> - private Vector4 ProjectPoint(Vector4 point) - { - // rotations should happen before the point is translated into camera space - Vector4 worldSpace = point; - - // rotations of point in the x, y and z axes - worldSpace *= zRotationMatrix; - worldSpace *= yRotationMatrix; - worldSpace *= xRotationMatrix; - - // points must be translated into the camera space, as the camera must be some distance away from the - // world space origin (0,0,0) or else rendering breaks. the point is translated into the middle of the - // camera space (the view frustum) - Vector4 cameraSpace = worldSpace + cameraTranslationVector; - - // project the point position from camera space into screen space (without any special transformations) - Vector4 projectedPosition = cameraSpace * projectionMatrix; - - if (!projectedPosition.W.Equals(0)) - { - projectedPosition.X /= projectedPosition.W; - projectedPosition.Y /= projectedPosition.W; - projectedPosition.Z /= projectedPosition.W; - } - - // any transformations can be applied now - Vector4 screenPosition = projectedPosition; - - return screenPosition; - } - - /// <summary> - /// Updates the rotation matrices for the view. - /// </summary> - private void UpdateRotationMatrices() - { - (double xAngle, double yAngle, double zAngle) = (rotation.X, rotation.Y, rotation.Z); - - // update the rotation matrix values for the x axis - xRotationMatrix[1, 1] = Math.Cos(xAngle * DegToRad); - xRotationMatrix[1, 2] = -Math.Sin(xAngle * DegToRad); - xRotationMatrix[2, 1] = Math.Sin(xAngle * DegToRad); - xRotationMatrix[2, 2] = Math.Cos(xAngle * DegToRad); - - // update the rotation matrix values for the y axis - yRotationMatrix[0, 0] = Math.Cos(yAngle * DegToRad); - yRotationMatrix[0, 2] = Math.Sin(yAngle * DegToRad); - yRotationMatrix[2, 0] = -Math.Sin(yAngle * DegToRad); - yRotationMatrix[2, 2] = Math.Cos(yAngle * DegToRad); - - // update the rotation matrix values for the z axis - zRotationMatrix[0, 0] = Math.Cos(zAngle * DegToRad); - zRotationMatrix[0, 1] = -Math.Sin(zAngle * DegToRad); - zRotationMatrix[1, 0] = Math.Sin(zAngle * DegToRad); - zRotationMatrix[1, 1] = Math.Cos(zAngle * DegToRad); - } - - /// <summary> - /// Draws each <see cref="Body"/> instance that is managed by this drawer to the <see cref="RenderTarget"/> specified - /// in the constructor, using the current view settings (rotation, zoom, etc.) to project the positions from 3D to 2D. - /// This method should be called every frame, as without it the view isn't updated and neither are rotation or zoom. - /// </summary> - public void DrawBodies() - { - foreach (Body body in managedBodies) - { - // get the cached shape and projects the current bodies 3D position down to a 2D position, - // which is used as the new position of the shape - CircleShape shape = managedBodyShapeMap[body]; - - // get the vertex array used to store orbit tracer points for this body - VertexArray orbitTracerVertexArray = managedBodyTracerVertexArrayMap[body]; - - // we need to clear the vertex array, to ensure that no garbage values are present. otherwise - // there are lines from the origin to the first actually desired position - orbitTracerVertexArray.Clear(); - - // we only attempt to draw tracers if the body instance in question should record its previous position - // otherwise, we skip the relatively expensive rendering code - if (body.RecordPreviousPositions) - { - Vector4[] orbitTracerPositions = body.OrbitTracer.PreviousPositions.ToArray(); - - // we cache the colours used for the orbit tracers and the background, and pack them into a 4D vector - Color tracerColour = Color.Cyan, bgColour = Color.Transparent; - Vector4 tracerVector = new Vector4(tracerColour.R, tracerColour.G, tracerColour.B, tracerColour.A); - Vector4 bgVector = new Vector4(bgColour.R, bgColour.G, bgColour.B, bgColour.A); - - for (uint i = 0; i < orbitTracerPositions.Length; i++) - { - Vector4 previousPosition = orbitTracerPositions[i]; - - // project previous position onto the screen - Vector4 pointScreenPosition = ProjectPoint(previousPosition); - - // convert the final position into a Vector2f, useable by the SFML.NET Vertex struct - Vector2f finalOrbitTracerPosition = new Vector2f( - (float)(pointScreenPosition.X * renderTarget.Size.X / 2 + originOffset.X), - (float)(pointScreenPosition.Y * renderTarget.Size.Y / 2 + originOffset.Y)); - - // use linear interpolation to make the tail colour transition look smooth - Vector4 interpolatedColourVector = - LinearInterpolate(tracerVector, bgVector, i / (double)orbitTracerPositions.Length); - - // unpack the colour from a vector into a colour - Color interpolatedColour = new Color( - (byte)interpolatedColourVector.X, - (byte)interpolatedColourVector.Y, - (byte)interpolatedColourVector.Z, - (byte)interpolatedColourVector.W - ); - - // append the new vertex to the orbit tracer array - orbitTracerVertexArray.Append(new Vertex(finalOrbitTracerPosition, interpolatedColour)); - } - - // single call to VertexArray.Draw() makes use of hardware acceleration without causing delays, - // as modern graphics processing units are optimised to render many vertices simultaneously, instead - // of rendering many vertices sequentially - orbitTracerVertexArray.Draw(renderTarget, RenderStates.Default); - } - - Vector4 screenPosition = ProjectPoint(body.Position); - - // final position - shape.Position = new Vector2f( - (float)(screenPosition.X * renderTarget.Size.X / 2 + originOffset.X), - (float)(screenPosition.Y * renderTarget.Size.Y / 2 + originOffset.Y) - ); - - // the shape is drawn onto the render target at its final screen position - shape.Draw(renderTarget, RenderStates.Default); - } - } - - /// <summary> - /// Rotates the view in the given direction, by the specified angle (in degrees). - /// </summary> - /// <param name="direction">The direction in which to rotate the view.</param> - /// <param name="angle">The angle by which to rotate in the given direction.</param> - public void Rotate(RotationDirection direction, double angle) - { - switch (direction) - { - case RotationDirection.North: - rotation.X += angle % 360; - break; - - case RotationDirection.East: - rotation.Y += angle % 360; - break; - - case RotationDirection.South: - rotation.X += 360 - angle % 360; - break; - - case RotationDirection.West: - rotation.Y += 360 - angle % 360; - break; - - case RotationDirection.Clockwise: - rotation.Z += 360 - angle % 360; - break; - - case RotationDirection.Anticlockwise: - rotation.Z += angle % 360; - break; - - default: - throw new ArgumentOutOfRangeException(nameof(direction), direction, - "The given rotation direction was not within the valid range."); - } - - // once we hit 360 degrees of rotation, we wrap back around to 0 degrees - rotation.X %= 360; - rotation.Y %= 360; - rotation.Z %= 360; - - UpdateRotationMatrices(); - } - - /// <summary> - /// Scales this instances view by the given amount. - /// </summary> - /// <param name="scaleMultiplier">The multiplier by which to scale the viewport of this instances render target.</param> - public void Scale(double scaleMultiplier) - { - currentFieldOfView /= scaleMultiplier; - - // limits field of view to a lower bound and an upper bound - currentFieldOfView = currentFieldOfView < 0.0001 ? 0.0001 : currentFieldOfView; - currentFieldOfView = currentFieldOfView > 179d ? 179 : currentFieldOfView; - - // reassign projection matrix fields as the field of view (used by InverseScaleFactor) has been - // modified to zoom in or out - projectionMatrix[0, 0] = aspectRatio * InverseScaleFactor; - projectionMatrix[1, 1] = InverseScaleFactor; - } - } -} -\ No newline at end of file diff --git a/StarSim/StarSimLib/Graphics/SimulationDrawer.cs b/StarSim/StarSimLib/Graphics/SimulationDrawer.cs @@ -0,0 +1,486 @@ +using System; +using System.Collections.Generic; +using SFML.Graphics; +using SFML.System; +using StarSimLib.Data_Structures; + +namespace StarSimLib.Graphics +{ + /// <summary> + /// Enumerates all possible rotation directions (i.e. north, east, south, west, clockwise, anticlockwise). + /// Here, camera forward is north. + /// </summary> + public enum RotationDirection + { + /// <summary> + /// Represents an anticlockwise rotation in the x-axis. + /// </summary> + North, + + /// <summary> + /// Represents an anticlockwise rotation in the y-axis. + /// </summary> + East, + + /// <summary> + /// Represents a clockwise rotation in the x-axis. + /// </summary> + South, + + /// <summary> + /// Represents a clockwise rotation in the y-axis. + /// </summary> + West, + + /// <summary> + /// Represents a clockwise rotation in the z-axis. + /// </summary> + Clockwise, + + /// <summary> + /// Represents an anticlockwise rotation in the z-axis. + /// </summary> + Anticlockwise + } + + /// <summary> + /// Represents a drawer capable of rendering bodies to a <see cref="RenderTarget"/>. + /// </summary> + public class SimulationDrawer + { + /// <summary> + /// Conversion from degrees to radians, as used by the <see cref="Math"/> functions. + /// </summary> + private const double DegToRad = Math.PI / 180; + + /// <summary> + /// The furthest distance that can be seen on the <see cref="RenderTarget"/>. Any bodies that are further + /// from the camera than this distance are culled and not rendered. + /// </summary> + private const double FarDistance = Constants.UniverseSize; + + /// <summary> + /// The default field of view for the drawer in degrees. + /// </summary> + private const double FieldOfView = 45; + + /// <summary> + /// The closest distance that can be seen on the <see cref="RenderTarget"/>. Any bodies that are closer + /// to the camera than this distance (i.e. behind the camera) are culled and not rendered. + /// </summary> + private const double NearDistance = 0; + + /// <summary> + /// The vector by which all projected points are translated away from the camera and halfway into the view frustum. + /// </summary> + private static readonly Vector4 cameraTranslationVector = new Vector4(0, 0, (FarDistance - NearDistance) / 2); + + /// <summary> + /// The aspect ratio of the render target. Allows for normalisation of the <see cref="RenderTarget"/> space + /// into a normal plane ([-1, -1] = top left, [1, 1] = bottom right), instead of having to deal with a + /// variable render space. + /// </summary> + private readonly double aspectRatio; + + /// <summary> + /// Holds all the <see cref="Body"/> instances that should be simulated. + /// </summary> + private readonly Body[] managedBodies; + + /// <summary> + /// Maps a <see cref="Body"/> to the <see cref="CircleShape"/> that represents it, and is drawn to the + /// screen at the <see cref="Body"/>s position. + /// </summary> + private readonly Dictionary<Body, CircleShape> managedBodyShapeMap; + + /// <summary> + /// Maps a <see cref="Body"/> to the <see cref="VertexArray"/> that holds its orbit tracer vertices, and + /// is drawn to the screen behind the <see cref="Body"/> instance. + /// </summary> + private readonly Dictionary<Body, VertexArray> managedBodyTracerVertexArrayMap; + + /// <summary> + /// The offset that has to be applied to the positions of a <see cref="CircleShape"/>, so that they appear + /// to be in the centre of the render target and not the top left corner. + /// </summary> + private readonly Vector2u originOffset; + + /// <summary> + /// The target to which the managed bodies will be drawn. + /// </summary> + private readonly RenderTarget renderTarget; + + /// <summary> + /// The current value for the field of view, in degrees. Used to zoom the view in and out. + /// </summary> + private double currentFieldOfView = FieldOfView; + +#pragma warning disable IDE0044 // Set field to readonly + + /// <summary> + /// Projection matrix used for mapping from 3D to 2D. + /// </summary> + private Matrix4x4 projectionMatrix; + + /// <summary> + /// The 3D rotation. + /// </summary> + private EulerAngle rotation; + + /// <summary> + /// The rotation matrix for the x axis. + /// </summary> + private Matrix4x4 xRotationMatrix; + + /// <summary> + /// The rotation matrix for the y axis. + /// </summary> + private Matrix4x4 yRotationMatrix; + + /// <summary> + /// The rotation matrix for the z axis. + /// </summary> + private Matrix4x4 zRotationMatrix; + +#pragma warning restore IDE0044 + + /// <summary> + /// Initialises a new instance of the <see cref="SimulationDrawer"/> class. + /// </summary> + /// <param name="target">The target to which the managed bodies should be rendered.</param> + /// <param name="bodies"> + /// A reference to the <see cref="Body"/> instances which should be managed by this instance. + /// </param> + /// <param name="bodyShapeMap"> + /// A reference to the dictionary mapping a <see cref="Body"/> instance to the <see cref="CircleShape"/> that + /// represents it, and is drawn to the screen at the <see cref="Body"/> instance's position. + /// </param> + public SimulationDrawer(RenderTarget target, ref Body[] bodies, ref Dictionary<Body, CircleShape> bodyShapeMap) + { + renderTarget = target; + originOffset = target.Size / 2; + + aspectRatio = renderTarget.Size.Y / (double)renderTarget.Size.X; + + // projection and rotation matrices + projectionMatrix = new Matrix4x4(new[] + { + new [] { aspectRatio * InverseScaleFactor, 0, 0, 0 }, + new [] { 0, InverseScaleFactor, 0, 0 }, + new [] { 0, 0, FarDistance / (FarDistance - NearDistance), 1 }, + new [] { 0, 0, -FarDistance * NearDistance / (FarDistance - NearDistance), 0 } + }); + + xRotationMatrix = new Matrix4x4(new[] + { + new [] { 1d, 0, 0, 0 }, + new [] { 0d, 0, 0, 0 }, + new [] { 0d, 0, 0, 0 }, + new [] { 0d, 0, 0, 1 } + }); + + yRotationMatrix = new Matrix4x4(new[] + { + new [] { 0d, 0, 0, 0 }, + new [] { 0d, 1, 0, 0 }, + new [] { 0d, 0, 0, 0 }, + new [] { 0d, 0, 0, 1 } + }); + + zRotationMatrix = new Matrix4x4(new[] + { + new [] { 0d, 0, 0, 0 }, + new [] { 0d, 0, 0, 0 }, + new [] { 0d, 0, 1, 0 }, + new [] { 0d, 0, 0, 1 } + }); + + // set initial values for the rotation matrices + UpdateRotationMatrices(); + + managedBodies = bodies; + managedBodyShapeMap = bodyShapeMap; + managedBodyTracerVertexArrayMap = new Dictionary<Body, VertexArray>(); + + foreach (Body body in bodies) + { + // create a vertex array to store orbit tracer points for this body + managedBodyTracerVertexArrayMap.Add(body, + new VertexArray(PrimitiveType.LineStrip, Constants.StoredPreviousPositionCount)); + } + } + + /// <summary> + /// Inverse scale factor used in the projection matrix. + /// </summary> + private double InverseScaleFactor { get { return 1 / Math.Tan(currentFieldOfView * 0.5 * DegToRad); } } + + /// <summary> + /// Current field-of-view. + /// </summary> + public double FOV + { + get + { + return currentFieldOfView; + } + } + + /// <summary> + /// Current angle of rotation in the x axis. + /// </summary> + public double XAngle + { + get { return rotation.X; } + } + + /// <summary> + /// Current angle of rotation in the x axis. + /// </summary> + public double YAngle + { + get { return rotation.Y; } + } + + /// <summary> + /// Current angle of rotation in the x axis. + /// </summary> + public double ZAngle + { + get { return rotation.Z; } + } + + /// <summary> + /// Current zoom level of the drawer. + /// </summary> + public double ZoomLevel + { + get { return FieldOfView / currentFieldOfView; } + } + + /// <summary> + /// Linearly interpolates between a and b by the given percentage dt (0 = 100% a, 1 = 100% b). + /// </summary> + /// <param name="a">One of the starting values between which to interpolate.</param> + /// <param name="b">One of the starting values between which to interpolate.</param> + /// <param name="dt">The percentage by which to interpolate, capped between 0 and 1.</param> + /// <returns>The interpolated value.</returns> + private static Vector4 LinearInterpolate(Vector4 a, Vector4 b, double dt) + { + // caps the percentage first between 1 and -Infinity, and then between 1 and 0, and reverses it such + // that 0 is 100% a and 0% b, and that 1 is 0% a and 100% b. + dt = 1 - dt; + dt = dt > 1 ? 1 : dt; + dt = dt < 0 ? 0 : dt; + + /* formula: C = A + dt(B - A) / distance */ + Vector4 c = a + dt * (b - a) / 1; + + return c; + } + + /// <summary> + /// Projects the given <see cref="Vector4"/> point from world space into screen space. + /// </summary> + /// <param name="point">The point to project.</param> + /// <returns>The projected point.</returns> + private Vector4 ProjectPoint(Vector4 point) + { + // rotations should happen before the point is translated into camera space + Vector4 worldSpace = point; + + // rotations of point in the x, y and z axes + worldSpace *= zRotationMatrix; + worldSpace *= yRotationMatrix; + worldSpace *= xRotationMatrix; + + // points must be translated into the camera space, as the camera must be some distance away from the + // world space origin (0,0,0) or else rendering breaks. the point is translated into the middle of the + // camera space (the view frustum) + Vector4 cameraSpace = worldSpace + cameraTranslationVector; + + // project the point position from camera space into screen space (without any special transformations) + Vector4 projectedPosition = cameraSpace * projectionMatrix; + + if (!projectedPosition.W.Equals(0)) + { + projectedPosition.X /= projectedPosition.W; + projectedPosition.Y /= projectedPosition.W; + projectedPosition.Z /= projectedPosition.W; + } + + // any transformations can be applied now + Vector4 screenPosition = projectedPosition; + + return screenPosition; + } + + /// <summary> + /// Updates the rotation matrices for the view. + /// </summary> + private void UpdateRotationMatrices() + { + (double xAngle, double yAngle, double zAngle) = (rotation.X, rotation.Y, rotation.Z); + + // update the rotation matrix values for the x axis + xRotationMatrix[1, 1] = Math.Cos(xAngle * DegToRad); + xRotationMatrix[1, 2] = -Math.Sin(xAngle * DegToRad); + xRotationMatrix[2, 1] = Math.Sin(xAngle * DegToRad); + xRotationMatrix[2, 2] = Math.Cos(xAngle * DegToRad); + + // update the rotation matrix values for the y axis + yRotationMatrix[0, 0] = Math.Cos(yAngle * DegToRad); + yRotationMatrix[0, 2] = Math.Sin(yAngle * DegToRad); + yRotationMatrix[2, 0] = -Math.Sin(yAngle * DegToRad); + yRotationMatrix[2, 2] = Math.Cos(yAngle * DegToRad); + + // update the rotation matrix values for the z axis + zRotationMatrix[0, 0] = Math.Cos(zAngle * DegToRad); + zRotationMatrix[0, 1] = -Math.Sin(zAngle * DegToRad); + zRotationMatrix[1, 0] = Math.Sin(zAngle * DegToRad); + zRotationMatrix[1, 1] = Math.Cos(zAngle * DegToRad); + } + + /// <summary> + /// Draws each <see cref="Body"/> instance that is managed by this drawer to the <see cref="RenderTarget"/> specified + /// in the constructor, using the current view settings (rotation, zoom, etc.) to project the positions from 3D to 2D. + /// This method should be called every frame, as without it the view isn't updated and neither are rotation or zoom. + /// </summary> + public void DrawBodies() + { + foreach (Body body in managedBodies) + { + // get the cached shape and projects the current bodies 3D position down to a 2D position, + // which is used as the new position of the shape + CircleShape shape = managedBodyShapeMap[body]; + + // get the vertex array used to store orbit tracer points for this body + VertexArray orbitTracerVertexArray = managedBodyTracerVertexArrayMap[body]; + + // we need to clear the vertex array, to ensure that no garbage values are present. otherwise + // there are lines from the origin to the first actually desired position + orbitTracerVertexArray.Clear(); + + // we only attempt to draw tracers if the body instance in question should record its previous position + // otherwise, we skip the relatively expensive rendering code + if (body.RecordPreviousPositions) + { + Vector4[] orbitTracerPositions = body.OrbitTracer.PreviousPositions.ToArray(); + + // we cache the colours used for the orbit tracers and the background, and pack them into a 4D vector + Color tracerColour = Color.Cyan, bgColour = Color.Transparent; + Vector4 tracerVector = new Vector4(tracerColour.R, tracerColour.G, tracerColour.B, tracerColour.A); + Vector4 bgVector = new Vector4(bgColour.R, bgColour.G, bgColour.B, bgColour.A); + + for (uint i = 0; i < orbitTracerPositions.Length; i++) + { + Vector4 previousPosition = orbitTracerPositions[i]; + + // project previous position onto the screen + Vector4 pointScreenPosition = ProjectPoint(previousPosition); + + // convert the final position into a Vector2f, useable by the SFML.NET Vertex struct + Vector2f finalOrbitTracerPosition = new Vector2f( + (float)(pointScreenPosition.X * renderTarget.Size.X / 2 + originOffset.X), + (float)(pointScreenPosition.Y * renderTarget.Size.Y / 2 + originOffset.Y)); + + // use linear interpolation to make the tail colour transition look smooth + Vector4 interpolatedColourVector = + LinearInterpolate(tracerVector, bgVector, i / (double)orbitTracerPositions.Length); + + // unpack the colour from a vector into a colour + Color interpolatedColour = new Color( + (byte)interpolatedColourVector.X, + (byte)interpolatedColourVector.Y, + (byte)interpolatedColourVector.Z, + (byte)interpolatedColourVector.W + ); + + // append the new vertex to the orbit tracer array + orbitTracerVertexArray.Append(new Vertex(finalOrbitTracerPosition, interpolatedColour)); + } + + // single call to VertexArray.Draw() makes use of hardware acceleration without causing delays, + // as modern graphics processing units are optimised to render many vertices simultaneously, instead + // of rendering many vertices sequentially + orbitTracerVertexArray.Draw(renderTarget, RenderStates.Default); + } + + Vector4 screenPosition = ProjectPoint(body.Position); + + // final position + shape.Position = new Vector2f( + (float)(screenPosition.X * renderTarget.Size.X / 2 + originOffset.X), + (float)(screenPosition.Y * renderTarget.Size.Y / 2 + originOffset.Y) + ); + + // the shape is drawn onto the render target at its final screen position + shape.Draw(renderTarget, RenderStates.Default); + } + } + + /// <summary> + /// Rotates the view in the given direction, by the specified angle (in degrees). + /// </summary> + /// <param name="direction">The direction in which to rotate the view.</param> + /// <param name="angle">The angle by which to rotate in the given direction.</param> + public void Rotate(RotationDirection direction, double angle) + { + switch (direction) + { + case RotationDirection.North: + rotation.X += angle % 360; + break; + + case RotationDirection.East: + rotation.Y += angle % 360; + break; + + case RotationDirection.South: + rotation.X += 360 - angle % 360; + break; + + case RotationDirection.West: + rotation.Y += 360 - angle % 360; + break; + + case RotationDirection.Clockwise: + rotation.Z += 360 - angle % 360; + break; + + case RotationDirection.Anticlockwise: + rotation.Z += angle % 360; + break; + + default: + throw new ArgumentOutOfRangeException(nameof(direction), direction, + "The given rotation direction was not within the valid range."); + } + + // once we hit 360 degrees of rotation, we wrap back around to 0 degrees + rotation.X %= 360; + rotation.Y %= 360; + rotation.Z %= 360; + + UpdateRotationMatrices(); + } + + /// <summary> + /// Scales this instances view by the given amount. + /// </summary> + /// <param name="scaleMultiplier">The multiplier by which to scale the viewport of this instances render target.</param> + public void Scale(double scaleMultiplier) + { + currentFieldOfView /= scaleMultiplier; + + // limits field of view to a lower bound and an upper bound + currentFieldOfView = currentFieldOfView < 0.0001 ? 0.0001 : currentFieldOfView; + currentFieldOfView = currentFieldOfView > 179d ? 179 : currentFieldOfView; + + // reassign projection matrix fields as the field of view (used by InverseScaleFactor) has been + // modified to zoom in or out + projectionMatrix[0, 0] = aspectRatio * InverseScaleFactor; + projectionMatrix[1, 1] = InverseScaleFactor; + } + } +} +\ No newline at end of file diff --git a/StarSim/StarSimLib/StarSimLib.xml b/StarSim/StarSimLib/StarSimLib.xml @@ -1112,106 +1112,106 @@ Represents an anticlockwise rotation in the z-axis. </summary> </member> - <member name="T:StarSimLib.Graphics.Drawer"> + <member name="T:StarSimLib.Graphics.SimulationDrawer"> <summary> Represents a drawer capable of rendering bodies to a <see cref="T:SFML.Graphics.RenderTarget"/>. </summary> </member> - <member name="F:StarSimLib.Graphics.Drawer.DegToRad"> + <member name="F:StarSimLib.Graphics.SimulationDrawer.DegToRad"> <summary> Conversion from degrees to radians, as used by the <see cref="T:System.Math"/> functions. </summary> </member> - <member name="F:StarSimLib.Graphics.Drawer.FarDistance"> + <member name="F:StarSimLib.Graphics.SimulationDrawer.FarDistance"> <summary> The furthest distance that can be seen on the <see cref="T:SFML.Graphics.RenderTarget"/>. Any bodies that are further from the camera than this distance are culled and not rendered. </summary> </member> - <member name="F:StarSimLib.Graphics.Drawer.FieldOfView"> + <member name="F:StarSimLib.Graphics.SimulationDrawer.FieldOfView"> <summary> The default field of view for the drawer in degrees. </summary> </member> - <member name="F:StarSimLib.Graphics.Drawer.NearDistance"> + <member name="F:StarSimLib.Graphics.SimulationDrawer.NearDistance"> <summary> The closest distance that can be seen on the <see cref="T:SFML.Graphics.RenderTarget"/>. Any bodies that are closer to the camera than this distance (i.e. behind the camera) are culled and not rendered. </summary> </member> - <member name="F:StarSimLib.Graphics.Drawer.cameraTranslationVector"> + <member name="F:StarSimLib.Graphics.SimulationDrawer.cameraTranslationVector"> <summary> The vector by which all projected points are translated away from the camera and halfway into the view frustum. </summary> </member> - <member name="F:StarSimLib.Graphics.Drawer.aspectRatio"> + <member name="F:StarSimLib.Graphics.SimulationDrawer.aspectRatio"> <summary> The aspect ratio of the render target. Allows for normalisation of the <see cref="T:SFML.Graphics.RenderTarget"/> space into a normal plane ([-1, -1] = top left, [1, 1] = bottom right), instead of having to deal with a variable render space. </summary> </member> - <member name="F:StarSimLib.Graphics.Drawer.managedBodies"> + <member name="F:StarSimLib.Graphics.SimulationDrawer.managedBodies"> <summary> Holds all the <see cref="T:StarSimLib.Data_Structures.Body"/> instances that should be simulated. </summary> </member> - <member name="F:StarSimLib.Graphics.Drawer.managedBodyShapeMap"> + <member name="F:StarSimLib.Graphics.SimulationDrawer.managedBodyShapeMap"> <summary> Maps a <see cref="T:StarSimLib.Data_Structures.Body"/> to the <see cref="T:SFML.Graphics.CircleShape"/> that represents it, and is drawn to the screen at the <see cref="T:StarSimLib.Data_Structures.Body"/>s position. </summary> </member> - <member name="F:StarSimLib.Graphics.Drawer.managedBodyTracerVertexArrayMap"> + <member name="F:StarSimLib.Graphics.SimulationDrawer.managedBodyTracerVertexArrayMap"> <summary> Maps a <see cref="T:StarSimLib.Data_Structures.Body"/> to the <see cref="T:SFML.Graphics.VertexArray"/> that holds its orbit tracer vertices, and is drawn to the screen behind the <see cref="T:StarSimLib.Data_Structures.Body"/> instance. </summary> </member> - <member name="F:StarSimLib.Graphics.Drawer.originOffset"> + <member name="F:StarSimLib.Graphics.SimulationDrawer.originOffset"> <summary> The offset that has to be applied to the positions of a <see cref="T:SFML.Graphics.CircleShape"/>, so that they appear to be in the centre of the render target and not the top left corner. </summary> </member> - <member name="F:StarSimLib.Graphics.Drawer.renderTarget"> + <member name="F:StarSimLib.Graphics.SimulationDrawer.renderTarget"> <summary> The target to which the managed bodies will be drawn. </summary> </member> - <member name="F:StarSimLib.Graphics.Drawer.currentFieldOfView"> + <member name="F:StarSimLib.Graphics.SimulationDrawer.currentFieldOfView"> <summary> The current value for the field of view, in degrees. Used to zoom the view in and out. </summary> </member> - <member name="F:StarSimLib.Graphics.Drawer.projectionMatrix"> + <member name="F:StarSimLib.Graphics.SimulationDrawer.projectionMatrix"> <summary> Projection matrix used for mapping from 3D to 2D. </summary> </member> - <member name="F:StarSimLib.Graphics.Drawer.rotation"> + <member name="F:StarSimLib.Graphics.SimulationDrawer.rotation"> <summary> The 3D rotation. </summary> </member> - <member name="F:StarSimLib.Graphics.Drawer.xRotationMatrix"> + <member name="F:StarSimLib.Graphics.SimulationDrawer.xRotationMatrix"> <summary> The rotation matrix for the x axis. </summary> </member> - <member name="F:StarSimLib.Graphics.Drawer.yRotationMatrix"> + <member name="F:StarSimLib.Graphics.SimulationDrawer.yRotationMatrix"> <summary> The rotation matrix for the y axis. </summary> </member> - <member name="F:StarSimLib.Graphics.Drawer.zRotationMatrix"> + <member name="F:StarSimLib.Graphics.SimulationDrawer.zRotationMatrix"> <summary> The rotation matrix for the z axis. </summary> </member> - <member name="M:StarSimLib.Graphics.Drawer.#ctor(SFML.Graphics.RenderTarget,StarSimLib.Data_Structures.Body[]@,System.Collections.Generic.Dictionary{StarSimLib.Data_Structures.Body,SFML.Graphics.CircleShape}@)"> + <member name="M:StarSimLib.Graphics.SimulationDrawer.#ctor(SFML.Graphics.RenderTarget,StarSimLib.Data_Structures.Body[]@,System.Collections.Generic.Dictionary{StarSimLib.Data_Structures.Body,SFML.Graphics.CircleShape}@)"> <summary> - Initialises a new instance of the <see cref="T:StarSimLib.Graphics.Drawer"/> class. + Initialises a new instance of the <see cref="T:StarSimLib.Graphics.SimulationDrawer"/> class. </summary> <param name="target">The target to which the managed bodies should be rendered.</param> <param name="bodies"> @@ -1222,37 +1222,37 @@ represents it, and is drawn to the screen at the <see cref="T:StarSimLib.Data_Structures.Body"/> instance's position. </param> </member> - <member name="P:StarSimLib.Graphics.Drawer.InverseScaleFactor"> + <member name="P:StarSimLib.Graphics.SimulationDrawer.InverseScaleFactor"> <summary> Inverse scale factor used in the projection matrix. </summary> </member> - <member name="P:StarSimLib.Graphics.Drawer.FOV"> + <member name="P:StarSimLib.Graphics.SimulationDrawer.FOV"> <summary> Current field-of-view. </summary> </member> - <member name="P:StarSimLib.Graphics.Drawer.XAngle"> + <member name="P:StarSimLib.Graphics.SimulationDrawer.XAngle"> <summary> Current angle of rotation in the x axis. </summary> </member> - <member name="P:StarSimLib.Graphics.Drawer.YAngle"> + <member name="P:StarSimLib.Graphics.SimulationDrawer.YAngle"> <summary> Current angle of rotation in the x axis. </summary> </member> - <member name="P:StarSimLib.Graphics.Drawer.ZAngle"> + <member name="P:StarSimLib.Graphics.SimulationDrawer.ZAngle"> <summary> Current angle of rotation in the x axis. </summary> </member> - <member name="P:StarSimLib.Graphics.Drawer.ZoomLevel"> + <member name="P:StarSimLib.Graphics.SimulationDrawer.ZoomLevel"> <summary> Current zoom level of the drawer. </summary> </member> - <member name="M:StarSimLib.Graphics.Drawer.LinearInterpolate(StarSimLib.Data_Structures.Vector4,StarSimLib.Data_Structures.Vector4,System.Double)"> + <member name="M:StarSimLib.Graphics.SimulationDrawer.LinearInterpolate(StarSimLib.Data_Structures.Vector4,StarSimLib.Data_Structures.Vector4,System.Double)"> <summary> Linearly interpolates between a and b by the given percentage dt (0 = 100% a, 1 = 100% b). </summary> @@ -1261,33 +1261,33 @@ <param name="dt">The percentage by which to interpolate, capped between 0 and 1.</param> <returns>The interpolated value.</returns> </member> - <member name="M:StarSimLib.Graphics.Drawer.ProjectPoint(StarSimLib.Data_Structures.Vector4)"> + <member name="M:StarSimLib.Graphics.SimulationDrawer.ProjectPoint(StarSimLib.Data_Structures.Vector4)"> <summary> Projects the given <see cref="T:StarSimLib.Data_Structures.Vector4"/> point from world space into screen space. </summary> <param name="point">The point to project.</param> <returns>The projected point.</returns> </member> - <member name="M:StarSimLib.Graphics.Drawer.UpdateRotationMatrices"> + <member name="M:StarSimLib.Graphics.SimulationDrawer.UpdateRotationMatrices"> <summary> Updates the rotation matrices for the view. </summary> </member> - <member name="M:StarSimLib.Graphics.Drawer.DrawBodies"> + <member name="M:StarSimLib.Graphics.SimulationDrawer.DrawBodies"> <summary> Draws each <see cref="T:StarSimLib.Data_Structures.Body"/> instance that is managed by this drawer to the <see cref="T:SFML.Graphics.RenderTarget"/> specified in the constructor, using the current view settings (rotation, zoom, etc.) to project the positions from 3D to 2D. This method should be called every frame, as without it the view isn't updated and neither are rotation or zoom. </summary> </member> - <member name="M:StarSimLib.Graphics.Drawer.Rotate(StarSimLib.Graphics.RotationDirection,System.Double)"> + <member name="M:StarSimLib.Graphics.SimulationDrawer.Rotate(StarSimLib.Graphics.RotationDirection,System.Double)"> <summary> Rotates the view in the given direction, by the specified angle (in degrees). </summary> <param name="direction">The direction in which to rotate the view.</param> <param name="angle">The angle by which to rotate in the given direction.</param> </member> - <member name="M:StarSimLib.Graphics.Drawer.Scale(System.Double)"> + <member name="M:StarSimLib.Graphics.SimulationDrawer.Scale(System.Double)"> <summary> Scales this instances view by the given amount. </summary> @@ -1604,84 +1604,150 @@ </summary> <returns>A 3D position vector.</returns> </member> - <member name="T:StarSimLib.UI.InputHandler"> + <member name="T:StarSimLib.UI.SimulationInputHandler"> <summary> Provides user input handling functions. </summary> </member> - <member name="F:StarSimLib.UI.InputHandler.bodyDrawer"> + <member name="F:StarSimLib.UI.SimulationInputHandler.managedBodies"> <summary> - The renderer used to display the <see cref="T:StarSimLib.Data_Structures.Body"/> instances on the screen. + Holds all the <see cref="T:StarSimLib.Data_Structures.Body"/> instances that should be simulated. </summary> </member> - <member name="F:StarSimLib.UI.InputHandler.managedBodies"> + <member name="F:StarSimLib.UI.SimulationInputHandler.simulationDrawer"> <summary> - Holds all the <see cref="T:StarSimLib.Data_Structures.Body"/> instances that should be simulated. + The renderer used to display the <see cref="T:StarSimLib.Data_Structures.Body"/> instances on the screen. </summary> </member> - <member name="M:StarSimLib.UI.InputHandler.#ctor(StarSimLib.Data_Structures.Body[]@,StarSimLib.Graphics.Drawer@)"> + <member name="M:StarSimLib.UI.SimulationInputHandler.#ctor(StarSimLib.Data_Structures.Body[]@,StarSimLib.Graphics.SimulationDrawer@)"> <summary> - Initialises a new instance of the <see cref="T:StarSimLib.UI.InputHandler"/> class. + Initialises a new instance of the <see cref="T:StarSimLib.UI.SimulationInputHandler"/> class. </summary> <param name="bodies"> A reference to the <see cref="T:StarSimLib.Data_Structures.Body"/> instances which should be managed by this instance. </param> - <param name="drawer"> + <param name="simulationDrawer"> A reference to the renderer used to display the <see cref="T:StarSimLib.Data_Structures.Body"/> instances on the screen. </param> </member> - <member name="P:StarSimLib.UI.InputHandler.IsSimulationPaused"> + <member name="P:StarSimLib.UI.SimulationInputHandler.IsSimulationPaused"> <summary> Whether the simulation is paused at any given time. </summary> </member> - <member name="P:StarSimLib.UI.InputHandler.RecordOrbitTracers"> + <member name="P:StarSimLib.UI.SimulationInputHandler.RecordOrbitTracers"> <summary> Whether to record previous <see cref="T:StarSimLib.Data_Structures.Body"/> instance positions, in order to render an orbit tracer behind each body instance. </summary> </member> - <member name="M:StarSimLib.UI.InputHandler.HandleKeyPressed(System.Object,SFML.Window.KeyEventArgs)"> + <member name="M:StarSimLib.UI.SimulationInputHandler.HandleKeyPressed(System.Object,SFML.Window.KeyEventArgs)"> <summary> Handles key presses. </summary> <param name="sender">The <see cref="T:SFML.Window.Window"/> that sent the event.</param> <param name="eventArgs">The <see cref="T:SFML.Window.KeyEventArgs"/> associated with the key press.</param> </member> - <member name="M:StarSimLib.UI.InputHandler.HandleKeyReleased(System.Object,SFML.Window.KeyEventArgs)"> + <member name="M:StarSimLib.UI.SimulationInputHandler.HandleKeyReleased(System.Object,SFML.Window.KeyEventArgs)"> <summary> Handles key releases. </summary> <param name="sender">The <see cref="T:SFML.Window.Window"/> that sent the event.</param> <param name="eventArgs">The <see cref="T:SFML.Window.KeyEventArgs"/> associated with the key release.</param> </member> - <member name="M:StarSimLib.UI.InputHandler.HandleMouseMoved(System.Object,SFML.Window.MouseMoveEventArgs)"> + <member name="M:StarSimLib.UI.SimulationInputHandler.HandleMouseMoved(System.Object,SFML.Window.MouseMoveEventArgs)"> <summary> Handles motions of the mouse. </summary> <param name="sender">The <see cref="T:SFML.Window.Window"/> that sent the event.</param> <param name="eventArgs">The <see cref="T:SFML.Window.MouseMoveEventArgs"/> associated with the mouse movement.</param> </member> - <member name="M:StarSimLib.UI.InputHandler.HandleMousePressed(System.Object,SFML.Window.MouseButtonEventArgs)"> + <member name="M:StarSimLib.UI.SimulationInputHandler.HandleMousePressed(System.Object,SFML.Window.MouseButtonEventArgs)"> <summary> Handles key presses of the mouse. </summary> <param name="sender">The <see cref="T:SFML.Window.Window"/> that sent the event.</param> <param name="eventArgs">The <see cref="T:SFML.Window.MouseButtonEventArgs"/> associated with the mouse press.</param> </member> - <member name="M:StarSimLib.UI.InputHandler.HandleMouseReleased(System.Object,SFML.Window.MouseButtonEventArgs)"> + <member name="M:StarSimLib.UI.SimulationInputHandler.HandleMouseReleased(System.Object,SFML.Window.MouseButtonEventArgs)"> <summary> Handles key releases of the mouse. </summary> <param name="sender">The <see cref="T:SFML.Window.Window"/> that sent the event.</param> <param name="eventArgs">The <see cref="T:SFML.Window.MouseButtonEventArgs"/> associated with the mouse release.</param> </member> - <member name="M:StarSimLib.UI.InputHandler.HandleMouseScrolled(System.Object,SFML.Window.MouseWheelScrollEventArgs)"> + <member name="M:StarSimLib.UI.SimulationInputHandler.HandleMouseScrolled(System.Object,SFML.Window.MouseWheelScrollEventArgs)"> <summary> Handles scrolling of the mouse wheel. </summary> <param name="sender">The <see cref="T:SFML.Window.Window"/> that sent the event.</param> <param name="eventArgs">The <see cref="T:SFML.Window.MouseWheelScrollEventArgs"/> associated with the mouse scroll.</param> </member> + <member name="T:StarSimLib.UI.SimulationScreen"> + <summary> + Encapsulates the simulation. + </summary> + </member> + <member name="F:StarSimLib.UI.SimulationScreen.TimerRefreshIntervalMs"> + <summary> + The interval between timer refreshes, in milliseconds. + </summary> + </member> + <member name="F:StarSimLib.UI.SimulationScreen.bodies"> + <summary> + Holds all the <see cref="T:StarSimLib.Data_Structures.Body"/> instances that should be simulated. + </summary> + </member> + <member name="F:StarSimLib.UI.SimulationScreen.bodyPositionUpdater"> + <summary> + The body position update algorithm to use. + </summary> + </member> + <member name="F:StarSimLib.UI.SimulationScreen.bodyShapeMap"> + <summary> + Maps a <see cref="T:StarSimLib.Data_Structures.Body"/> to the <see cref="T:SFML.Graphics.CircleShape"/> that represents it, and is drawn to the + screen at the <see cref="T:StarSimLib.Data_Structures.Body"/>s position. + </summary> + </member> + <member name="F:StarSimLib.UI.SimulationScreen.miscTimer"> + <summary> + Timer that manages FPS counter and other miscellaneous counters. + </summary> + </member> + <member name="F:StarSimLib.UI.SimulationScreen.simulationDrawer"> + <summary> + The renderer used to display the <see cref="T:StarSimLib.Data_Structures.Body"/> instances on the screen. + </summary> + </member> + <member name="F:StarSimLib.UI.SimulationScreen.simulationInputHandler"> + <summary> + The input handler to use to provide interactivity to the simulator. + </summary> + </member> + <member name="F:StarSimLib.UI.SimulationScreen.window"> + <summary> + The SFML.NET window to which everything is rendered. + </summary> + </member> + <member name="F:StarSimLib.UI.SimulationScreen.fps"> + <summary> + The current amount of frames per second. + </summary> + </member> + <member name="F:StarSimLib.UI.SimulationScreen.framesElapsed"> + <summary> + Counts the frames elapsed since the last timer pulse, so that the FPS can be tracked. + </summary> + </member> + <member name="M:StarSimLib.UI.SimulationScreen.#ctor(StarSimLib.Data_Structures.Body[]@,System.Collections.Generic.Dictionary{StarSimLib.Data_Structures.Body,SFML.Graphics.CircleShape}@,StarSimLib.Physics.UpdateDelegate)"> + <summary> + Initialises a new instance of the <see cref="T:StarSimLib.UI.SimulationScreen"/> class, + </summary> + </member> + <member name="M:StarSimLib.UI.SimulationScreen.Run"> + <summary> + Runs the screen until it is closed. + </summary> + </member> </members> </doc> diff --git a/StarSim/StarSimLib/UI/InputHandler.cs b/StarSim/StarSimLib/UI/InputHandler.cs @@ -1,211 +0,0 @@ -using System; -using SFML.Window; -using StarSimLib.Data_Structures; -using StarSimLib.Graphics; - -namespace StarSimLib.UI -{ - /// <summary> - /// Provides user input handling functions. - /// </summary> - public class InputHandler - { - /// <summary> - /// The renderer used to display the <see cref="Body"/> instances on the screen. - /// </summary> - private readonly Drawer bodyDrawer; - - /// <summary> - /// Holds all the <see cref="Body"/> instances that should be simulated. - /// </summary> - private readonly Body[] managedBodies; - - /// <summary> - /// Initialises a new instance of the <see cref="InputHandler"/> class. - /// </summary> - /// <param name="bodies"> - /// A reference to the <see cref="Body"/> instances which should be managed by this instance. - /// </param> - /// <param name="drawer"> - /// A reference to the renderer used to display the <see cref="Body"/> instances on the screen. - /// </param> - public InputHandler(ref Body[] bodies, ref Drawer drawer) - { - managedBodies = bodies; - bodyDrawer = drawer; - } - - /// <summary> - /// Whether the simulation is paused at any given time. - /// </summary> - public bool IsSimulationPaused { get; set; } - - /// <summary> - /// Whether to record previous <see cref="Body"/> instance positions, in order to render an orbit tracer - /// behind each body instance. - /// </summary> - public bool RecordOrbitTracers { get; set; } - - /// <summary> - /// Handles key presses. - /// </summary> - /// <param name="sender">The <see cref="Window"/> that sent the event.</param> - /// <param name="eventArgs">The <see cref="KeyEventArgs"/> associated with the key press.</param> - public void HandleKeyPressed(object sender, KeyEventArgs eventArgs) - { - string msg = ""; - - switch (eventArgs.Code) - { - case Keyboard.Key.Space: - // toggle the paused state - IsSimulationPaused = !IsSimulationPaused; - break; - - case Keyboard.Key.T: - // toggle orbit tracer recording - RecordOrbitTracers = !RecordOrbitTracers; - - // every simulated body must be individually set to record tracers - foreach (Body body in managedBodies) - { - // we want to clear the screen of any orbit tracers that still exist when we turn orbit tracing - // off, so we must clear the previous position queues of every managed object. this has the - // effect of clearing the vertex arrays holding the orbit tracer vertices during the next draw step - body.RecordPreviousPositions = RecordOrbitTracers; - body.OrbitTracer.Clear(); - } - - break; - - case Keyboard.Key.W: - // rotate the view north - bodyDrawer.Rotate(RotationDirection.North, Constants.EulerRotationStep); - msg = $"Rotated by {Constants.EulerRotationStep} degrees anticlockwise in the x axis. " + - $"View Rotation: ({bodyDrawer.XAngle},{bodyDrawer.YAngle},{bodyDrawer.ZAngle})"; - break; - - case Keyboard.Key.A: - // rotate the view west - bodyDrawer.Rotate(RotationDirection.West, Constants.EulerRotationStep); - msg = $"Rotated by {Constants.EulerRotationStep} degrees clockwise in the y axis. " + - $"View Rotation: ({bodyDrawer.XAngle},{bodyDrawer.YAngle},{bodyDrawer.ZAngle})"; - break; - - case Keyboard.Key.S: - // rotate the view south - bodyDrawer.Rotate(RotationDirection.South, Constants.EulerRotationStep); - msg = $"Rotated by {Constants.EulerRotationStep} degrees clockwise in the x axis. " + - $"View Rotation: ({bodyDrawer.XAngle},{bodyDrawer.YAngle},{bodyDrawer.ZAngle})"; - break; - - case Keyboard.Key.D: - // rotate the view east - bodyDrawer.Rotate(RotationDirection.East, Constants.EulerRotationStep); - msg = $"Rotated by {Constants.EulerRotationStep} degrees anticlockwise in the y axis. " + - $"View Rotation: ({bodyDrawer.XAngle},{bodyDrawer.YAngle},{bodyDrawer.ZAngle})"; - break; - - case Keyboard.Key.Q: - // rotate the view anti-clockwise - bodyDrawer.Rotate(RotationDirection.Anticlockwise, Constants.EulerRotationStep); - msg = $"Rotated by {Constants.EulerRotationStep} degrees anticlockwise in the z axis. " + - $"View Rotation: ({bodyDrawer.XAngle},{bodyDrawer.YAngle},{bodyDrawer.ZAngle})"; - break; - - case Keyboard.Key.E: - // rotate the view clockwise - bodyDrawer.Rotate(RotationDirection.Clockwise, Constants.EulerRotationStep); - msg = $"Rotated by {Constants.EulerRotationStep} degrees anticlockwise in the z axis. " + - $"View Rotation: ({bodyDrawer.XAngle},{bodyDrawer.YAngle},{bodyDrawer.ZAngle})"; - break; - - case Keyboard.Key.Comma: - // decrease the simulation speed - break; - - case Keyboard.Key.Period: - // increase the simulation speed - break; - - default: - break; - } - - if (!msg.Equals("")) - { - Console.WriteLine(msg); - } - } - - /// <summary> - /// Handles key releases. - /// </summary> - /// <param name="sender">The <see cref="Window"/> that sent the event.</param> - /// <param name="eventArgs">The <see cref="KeyEventArgs"/> associated with the key release.</param> - public void HandleKeyReleased(object sender, KeyEventArgs eventArgs) - { - string msg = ""; - - switch (eventArgs.Code) - { - default: - break; - } - - if (!msg.Equals("")) - { - Console.WriteLine(msg); - } - } - - /// <summary> - /// Handles motions of the mouse. - /// </summary> - /// <param name="sender">The <see cref="Window"/> that sent the event.</param> - /// <param name="eventArgs">The <see cref="MouseMoveEventArgs"/> associated with the mouse movement.</param> - public void HandleMouseMoved(object sender, MouseMoveEventArgs eventArgs) - { - Console.WriteLine($"Mouse moved: {eventArgs.X} {eventArgs.Y}"); - } - - /// <summary> - /// Handles key presses of the mouse. - /// </summary> - /// <param name="sender">The <see cref="Window"/> that sent the event.</param> - /// <param name="eventArgs">The <see cref="MouseButtonEventArgs"/> associated with the mouse press.</param> - public void HandleMousePressed(object sender, MouseButtonEventArgs eventArgs) - { - Console.WriteLine($"Mouse pressed: {eventArgs.X} {eventArgs.Y}, {eventArgs.Button}"); - } - - /// <summary> - /// Handles key releases of the mouse. - /// </summary> - /// <param name="sender">The <see cref="Window"/> that sent the event.</param> - /// <param name="eventArgs">The <see cref="MouseButtonEventArgs"/> associated with the mouse release.</param> - public void HandleMouseReleased(object sender, MouseButtonEventArgs eventArgs) - { - Console.WriteLine($"Mouse released: {eventArgs.X} {eventArgs.Y}, {eventArgs.Button}"); - } - - /// <summary> - /// Handles scrolling of the mouse wheel. - /// </summary> - /// <param name="sender">The <see cref="Window"/> that sent the event.</param> - /// <param name="eventArgs">The <see cref="MouseWheelScrollEventArgs"/> associated with the mouse scroll.</param> - public void HandleMouseScrolled(object sender, MouseWheelScrollEventArgs eventArgs) - { - if (eventArgs.Delta > 0) - { - bodyDrawer.Scale(1 + Constants.ZoomStep); - } - else if (eventArgs.Delta < 0) - { - bodyDrawer.Scale(1 - Constants.ZoomStep); - } - - Console.WriteLine($"Current field of view (zoom level): {bodyDrawer.FOV} ({bodyDrawer.ZoomLevel})"); - } - } -} -\ No newline at end of file diff --git a/StarSim/StarSimLib/UI/SimulationInputHandler.cs b/StarSim/StarSimLib/UI/SimulationInputHandler.cs @@ -0,0 +1,211 @@ +using System; +using SFML.Window; +using StarSimLib.Data_Structures; +using StarSimLib.Graphics; + +namespace StarSimLib.UI +{ + /// <summary> + /// Provides user input handling functions. + /// </summary> + public class SimulationInputHandler + { + /// <summary> + /// Holds all the <see cref="Body"/> instances that should be simulated. + /// </summary> + private readonly Body[] managedBodies; + + /// <summary> + /// The renderer used to display the <see cref="Body"/> instances on the screen. + /// </summary> + private readonly SimulationDrawer simulationDrawer; + + /// <summary> + /// Initialises a new instance of the <see cref="SimulationInputHandler"/> class. + /// </summary> + /// <param name="bodies"> + /// A reference to the <see cref="Body"/> instances which should be managed by this instance. + /// </param> + /// <param name="simulationDrawer"> + /// A reference to the renderer used to display the <see cref="Body"/> instances on the screen. + /// </param> + public SimulationInputHandler(ref Body[] bodies, ref SimulationDrawer simulationDrawer) + { + managedBodies = bodies; + this.simulationDrawer = simulationDrawer; + } + + /// <summary> + /// Whether the simulation is paused at any given time. + /// </summary> + public bool IsSimulationPaused { get; set; } + + /// <summary> + /// Whether to record previous <see cref="Body"/> instance positions, in order to render an orbit tracer + /// behind each body instance. + /// </summary> + public bool RecordOrbitTracers { get; set; } + + /// <summary> + /// Handles key presses. + /// </summary> + /// <param name="sender">The <see cref="Window"/> that sent the event.</param> + /// <param name="eventArgs">The <see cref="KeyEventArgs"/> associated with the key press.</param> + public void HandleKeyPressed(object sender, KeyEventArgs eventArgs) + { + string msg = ""; + + switch (eventArgs.Code) + { + case Keyboard.Key.Space: + // toggle the paused state + IsSimulationPaused = !IsSimulationPaused; + break; + + case Keyboard.Key.T: + // toggle orbit tracer recording + RecordOrbitTracers = !RecordOrbitTracers; + + // every simulated body must be individually set to record tracers + foreach (Body body in managedBodies) + { + // we want to clear the screen of any orbit tracers that still exist when we turn orbit tracing + // off, so we must clear the previous position queues of every managed object. this has the + // effect of clearing the vertex arrays holding the orbit tracer vertices during the next draw step + body.RecordPreviousPositions = RecordOrbitTracers; + body.OrbitTracer.Clear(); + } + + break; + + case Keyboard.Key.W: + // rotate the view north + simulationDrawer.Rotate(RotationDirection.North, Constants.EulerRotationStep); + msg = $"Rotated by {Constants.EulerRotationStep} degrees anticlockwise in the x axis. " + + $"View Rotation: ({simulationDrawer.XAngle},{simulationDrawer.YAngle},{simulationDrawer.ZAngle})"; + break; + + case Keyboard.Key.A: + // rotate the view west + simulationDrawer.Rotate(RotationDirection.West, Constants.EulerRotationStep); + msg = $"Rotated by {Constants.EulerRotationStep} degrees clockwise in the y axis. " + + $"View Rotation: ({simulationDrawer.XAngle},{simulationDrawer.YAngle},{simulationDrawer.ZAngle})"; + break; + + case Keyboard.Key.S: + // rotate the view south + simulationDrawer.Rotate(RotationDirection.South, Constants.EulerRotationStep); + msg = $"Rotated by {Constants.EulerRotationStep} degrees clockwise in the x axis. " + + $"View Rotation: ({simulationDrawer.XAngle},{simulationDrawer.YAngle},{simulationDrawer.ZAngle})"; + break; + + case Keyboard.Key.D: + // rotate the view east + simulationDrawer.Rotate(RotationDirection.East, Constants.EulerRotationStep); + msg = $"Rotated by {Constants.EulerRotationStep} degrees anticlockwise in the y axis. " + + $"View Rotation: ({simulationDrawer.XAngle},{simulationDrawer.YAngle},{simulationDrawer.ZAngle})"; + break; + + case Keyboard.Key.Q: + // rotate the view anti-clockwise + simulationDrawer.Rotate(RotationDirection.Anticlockwise, Constants.EulerRotationStep); + msg = $"Rotated by {Constants.EulerRotationStep} degrees anticlockwise in the z axis. " + + $"View Rotation: ({simulationDrawer.XAngle},{simulationDrawer.YAngle},{simulationDrawer.ZAngle})"; + break; + + case Keyboard.Key.E: + // rotate the view clockwise + simulationDrawer.Rotate(RotationDirection.Clockwise, Constants.EulerRotationStep); + msg = $"Rotated by {Constants.EulerRotationStep} degrees anticlockwise in the z axis. " + + $"View Rotation: ({simulationDrawer.XAngle},{simulationDrawer.YAngle},{simulationDrawer.ZAngle})"; + break; + + case Keyboard.Key.Comma: + // decrease the simulation speed + break; + + case Keyboard.Key.Period: + // increase the simulation speed + break; + + default: + break; + } + + if (!msg.Equals("")) + { + Console.WriteLine(msg); + } + } + + /// <summary> + /// Handles key releases. + /// </summary> + /// <param name="sender">The <see cref="Window"/> that sent the event.</param> + /// <param name="eventArgs">The <see cref="KeyEventArgs"/> associated with the key release.</param> + public void HandleKeyReleased(object sender, KeyEventArgs eventArgs) + { + string msg = ""; + + switch (eventArgs.Code) + { + default: + break; + } + + if (!msg.Equals("")) + { + Console.WriteLine(msg); + } + } + + /// <summary> + /// Handles motions of the mouse. + /// </summary> + /// <param name="sender">The <see cref="Window"/> that sent the event.</param> + /// <param name="eventArgs">The <see cref="MouseMoveEventArgs"/> associated with the mouse movement.</param> + public void HandleMouseMoved(object sender, MouseMoveEventArgs eventArgs) + { + Console.WriteLine($"Mouse moved: {eventArgs.X} {eventArgs.Y}"); + } + + /// <summary> + /// Handles key presses of the mouse. + /// </summary> + /// <param name="sender">The <see cref="Window"/> that sent the event.</param> + /// <param name="eventArgs">The <see cref="MouseButtonEventArgs"/> associated with the mouse press.</param> + public void HandleMousePressed(object sender, MouseButtonEventArgs eventArgs) + { + Console.WriteLine($"Mouse pressed: {eventArgs.X} {eventArgs.Y}, {eventArgs.Button}"); + } + + /// <summary> + /// Handles key releases of the mouse. + /// </summary> + /// <param name="sender">The <see cref="Window"/> that sent the event.</param> + /// <param name="eventArgs">The <see cref="MouseButtonEventArgs"/> associated with the mouse release.</param> + public void HandleMouseReleased(object sender, MouseButtonEventArgs eventArgs) + { + Console.WriteLine($"Mouse released: {eventArgs.X} {eventArgs.Y}, {eventArgs.Button}"); + } + + /// <summary> + /// Handles scrolling of the mouse wheel. + /// </summary> + /// <param name="sender">The <see cref="Window"/> that sent the event.</param> + /// <param name="eventArgs">The <see cref="MouseWheelScrollEventArgs"/> associated with the mouse scroll.</param> + public void HandleMouseScrolled(object sender, MouseWheelScrollEventArgs eventArgs) + { + if (eventArgs.Delta > 0) + { + simulationDrawer.Scale(1 + Constants.ZoomStep); + } + else if (eventArgs.Delta < 0) + { + simulationDrawer.Scale(1 - Constants.ZoomStep); + } + + Console.WriteLine($"Current field of view (zoom level): {simulationDrawer.FOV} ({simulationDrawer.ZoomLevel})"); + } + } +} +\ No newline at end of file diff --git a/StarSim/StarSimLib/UI/SimulationScreen.cs b/StarSim/StarSimLib/UI/SimulationScreen.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Timers; +using SFML.Graphics; +using SFML.Window; +using StarSimLib.Contexts; +using StarSimLib.Data_Structures; +using StarSimLib.Graphics; +using StarSimLib.Physics; + +namespace StarSimLib.UI +{ + /// <summary> + /// Encapsulates the simulation. + /// </summary> + public class SimulationScreen + { + /// <summary> + /// The interval between timer refreshes, in milliseconds. + /// </summary> + private const double TimerRefreshIntervalMs = 500; + + /// <summary> + /// Holds all the <see cref="Body"/> instances that should be simulated. + /// </summary> + private readonly Body[] bodies; + + /// <summary> + /// The body position update algorithm to use. + /// </summary> + private readonly UpdateDelegate bodyPositionUpdater; + + /// <summary> + /// Maps a <see cref="Body"/> to the <see cref="CircleShape"/> that represents it, and is drawn to the + /// screen at the <see cref="Body"/>s position. + /// </summary> + private readonly Dictionary<Body, CircleShape> bodyShapeMap; + + /// <summary> + /// Timer that manages FPS counter and other miscellaneous counters. + /// </summary> + private readonly Timer miscTimer; + + /// <summary> + /// The renderer used to display the <see cref="Body"/> instances on the screen. + /// </summary> + private readonly SimulationDrawer simulationDrawer; + + /// <summary> + /// The input handler to use to provide interactivity to the simulator. + /// </summary> + private readonly SimulationInputHandler simulationInputHandler; + + /// <summary> + /// The SFML.NET window to which everything is rendered. + /// </summary> + private readonly RenderWindow window; + + /// <summary> + /// The current amount of frames per second. + /// </summary> + private double fps; + + /// <summary> + /// Counts the frames elapsed since the last timer pulse, so that the FPS can be tracked. + /// </summary> + private uint framesElapsed; + + /// <summary> + /// Initialises a new instance of the <see cref="SimulationScreen"/> class, + /// </summary> + public SimulationScreen(ref Body[] bodies, ref Dictionary<Body, CircleShape> bodyShapeMap, UpdateDelegate bodyPositionUpdater) + { + // we construct a new window instance, but immediately hide it so that we can configure the rest of the app + window = new RenderWindow(VideoMode.DesktopMode, "N-Body Simulation: FPS ", Styles.Default, new ContextSettings()); + window.SetVisible(false); + + this.bodies = bodies; + this.bodyShapeMap = bodyShapeMap; + + this.bodyPositionUpdater = bodyPositionUpdater; + + simulationDrawer = new SimulationDrawer(window, ref bodies, ref bodyShapeMap); + simulationInputHandler = new SimulationInputHandler(ref bodies, ref simulationDrawer); + + // constructs a new timer and attaches a timer event handler that updates the fps and window title every interval + miscTimer = new Timer(TimerRefreshIntervalMs) { AutoReset = true, Enabled = true }; + miscTimer.Elapsed += (sender, args) => + { + fps = framesElapsed / (TimerRefreshIntervalMs / 1000); + + framesElapsed = 0; + window.SetTitle($"N-Body Simulator: FPS {fps}"); + }; + } + + /// <summary> + /// Runs the screen until it is closed. + /// </summary> + public void Run() + { + // we reveal the window so that the user may interact with our program + window.SetVisible(true); + miscTimer.Start(); + + // we configure the window and its close handler, so that we may close the window once we are done with it + window.SetFramerateLimit(Constants.FrameRate); + window.Closed += (sender, eventArgs) => ((RenderWindow)sender).Close(); + + // we apply event handlers to allow for interactivity inside the window + window.KeyPressed += simulationInputHandler.HandleKeyPressed; + //window.KeyReleased += inputHandler.HandleKeyReleased; + window.MouseButtonPressed += simulationInputHandler.HandleMousePressed; + window.MouseButtonReleased += simulationInputHandler.HandleMouseReleased; + //window.MouseMoved += inputHandler.HandleMouseMoved; + window.MouseWheelScrolled += simulationInputHandler.HandleMouseScrolled; + + simulationDrawer.DrawBodies(); + + while (window.IsOpen) + { + window.Clear(); + window.DispatchEvents(); + + if (!simulationInputHandler.IsSimulationPaused) + { + bodyPositionUpdater(bodies, Constants.TimeStep); + } + + simulationDrawer.DrawBodies(); + + window.Display(); + + // increment the fps counter + framesElapsed++; + } + + miscTimer.Stop(); + } + } +} +\ No newline at end of file