After a relative hiatus of around two years, I've decided to start delving back into the programming world; to start, I've appropriated one of my older projects, CLIGL, and have made a solar system simulator that runs exclusively within a console interface.
There are several classes of importance, whose purposes and functions should be highlighted before the presentation of the code itself:
Body
: this class is responsible for storing the rendering and physics properties of a gravitational body, performing numerical integration of the position and velocity properties (viaIntegratePosition
andIntegrateVelocity
), and rendering the body itself.BodySystem
: this class is responsible for maintaining a list of gravitational bodies and simulating them properly; it will also render each body by calling theirRender
methods. Additionally, it will also render a set of physics properties for each simulated body in the top left corner of the console window.EntryPoint
: fairly self-explanatory; this class contains the initialization for and the main loop of the simulation, as well as a few constants. It also contains two helper methods for generating a decorative field of stars in the background.
EntryPoint.cs
using System;
using System.Diagnostics;
using System.Collections.Generic;
using CLIGL;
namespace GravitySimulator
{
/// <summary>
/// Class containing the program entry point, several constants/statics, and several
/// helper functions which have no other place.
/// </summary>
public class EntryPoint
{
public const int WINDOW_WIDTH = 159;
public const int WINDOW_HEIGHT = 60;
public static (bool, char)[] STAR_FIELD = new (bool, char)[WINDOW_WIDTH * WINDOW_HEIGHT];
/// <summary>
/// Randomly populate the star field with boolean values; false indicates no star, whereas
/// true indicates that a star is present. A character value will also be randomly generated.
/// </summary>
/// <param name="chanceOfStar">The chance that a star will be generated, from 0-100.</param>
/// <param name="starTypes">The different types of stars.</param>
public static void PopulateStarField(float chanceOfStar, char[] starTypes)
{
Random randomGenerator = new Random();
for(int i = 0; i < WINDOW_WIDTH * WINDOW_HEIGHT; i++)
{
STAR_FIELD[i] = (
randomGenerator.Next(0, 100) <= chanceOfStar,
starTypes[randomGenerator.Next(0, starTypes.Length)]
);
}
}
/// <summary>
/// Render the previously-generated random star field.
/// </summary>
/// <param name="buffer">The buffer to which the star field is to be rendered.</param>
public static void RenderStarField(ref RenderingBuffer buffer)
{
for(int i = 0; i < WINDOW_WIDTH * WINDOW_HEIGHT; i++)
{
if(STAR_FIELD[i].Item1)
{
buffer.SetPixel(i, new RenderingPixel(STAR_FIELD[i].Item2, ConsoleColor.DarkGray, ConsoleColor.Black));
}
}
}
/// <summary>
/// Application entry point.
/// </summary>
/// <param name="args">Command line arguments.</param>
public static void Main(string[] args)
{
RenderingWindow window = new RenderingWindow("Gravity Simulator", WINDOW_WIDTH, WINDOW_HEIGHT);
RenderingBuffer buffer = new RenderingBuffer(WINDOW_WIDTH, WINDOW_HEIGHT);
Stopwatch timeAccumulator = new Stopwatch();
timeAccumulator.Start();
float oldTime = (float)timeAccumulator.Elapsed.TotalSeconds;
float newTime;
float dt;
PopulateStarField(1, new char[] { '.', ',', '`', '\'' });
BodySystem system = new BodySystem(
new List<Body>() {
new Body("Star", '@', ConsoleColor.Yellow, ConsoleColor.Red, 10000, 0, 0, 0, 0),
new Body("Planet 1", '@', ConsoleColor.Gray, ConsoleColor.DarkGray, 10, 20, 0, 0, 35),
new Body("Planet 2", '@', ConsoleColor.DarkGray, ConsoleColor.Gray, 10, -30, 0, 0, -25),
new Body("Planet 3", '@', ConsoleColor.White, ConsoleColor.Gray, 10, -50, 0, 0, -15)
}
);
while(true)
{
newTime = (float)timeAccumulator.Elapsed.TotalSeconds;
dt = newTime - oldTime;
buffer.ClearPixelBuffer(RenderingPixel.EmptyPixel);
RenderStarField(ref buffer);
buffer.SetString(0, 0, $"DT :: {dt}", ConsoleColor.White, ConsoleColor.Black);
system.RenderBodies(ref buffer);
window.Render(buffer);
system.SimulateBodies(dt);
oldTime = newTime;
}
}
}
}
Body.cs
using System;
using CLIGL;
namespace GravitySimulator
{
/// <summary>
/// Contains position, velocity, and rendering information pertaining to a gravitational body,
/// simulated by the System class.
/// </summary>
public class Body
{
public const float G = 0.1f;
public string Name { get; set; }
public char Character { get; set; }
public ConsoleColor ForegroundColor { get; set; }
public ConsoleColor BackgroundColor { get; set; }
public float Mass { get; set; }
public float X { get; set; }
public float Y { get; set; }
public float Vx { get; set; }
public float Vy { get; set; }
public float Ax { get; set; }
public float Ay { get; set; }
/// <summary>
/// Constructor for the Body class.
/// </summary>
/// <param name="name">The name of the body.</param>
/// <param name="character">The rendering character for the body.</param>
/// <param name="foregroundColor">The foreground color of the body.</param>
/// <param name="backgroundColor">The background color of the body.</param>
/// <param name="mass">The mass of the body.</param>
/// <param name="x">The initial X position of the body.</param>
/// <param name="y">The initial Y position of the body.</param>
/// <param name="vx">The initial X velocity of the body.</param>
/// <param name="vy">The initial Y velocity of the body.</param>
public Body(
string name,
char character,
ConsoleColor foregroundColor,
ConsoleColor backgroundColor,
float mass,
float x,
float y,
float vx,
float vy
)
{
this.Name = name;
this.Character = character;
this.ForegroundColor = foregroundColor;
this.BackgroundColor = backgroundColor;
this.Mass = mass;
this.X = x;
this.Y = y;
this.Vx = vx;
this.Vy = vy;
this.Ax = 0.0f;
this.Ay = 0.0f;
}
/// <summary>
/// Integrate the position of the Body; this is done by adding the current velocities,
/// Vx and Vy, to the current positions, X and Y.
/// </summary>
/// <param name="dt">The amount of time since the last frame, delta time.</param>
public void IntegratePosition(float dt)
{
this.X += this.Vx * dt;
this.Y += this.Vy * dt;
}
/// <summary>
/// Integrate the velocity of the Body; this is done by calculating the attractive force
/// between the current body and a provided body, then deriving the accelerations, and then
/// adding the derived accelerations to the current velocities.
/// </summary>
/// <param name="otherBody">The body to which attraction is calculated.</param>
/// <param name="dt">The amount of time since the last frame, delta time.</param>
public void IntegrateVelocity(Body otherBody, float dt)
{
float distance = this.DistanceBetween(otherBody);
float directionX = (otherBody.X - this.X) / distance;
float directionY = (otherBody.Y - this.Y) / distance;
float attractiveForce = (G * this.Mass * otherBody.Mass) / distance;
float acceleration = attractiveForce / this.Mass;
float aX = acceleration * directionX;
float aY = acceleration * directionY;
this.Ax = aX;
this.Ay = aY;
this.Vx += this.Ax * dt;
this.Vy += this.Ay * dt;
}
/// <summary>
/// Find the distance between the current body and another body.
/// </summary>
/// <param name="otherBody">The other body.</param>
/// <returns>A distance value.</returns>
public float DistanceBetween(Body otherBody)
{
return (float)Math.Sqrt(
Math.Pow(otherBody.X - this.X, 2) +
Math.Pow(otherBody.Y - this.Y, 2)
);
}
/// <summary>
/// Render the body to a provided buffer.
/// </summary>
/// <remarks>This will render the body relative to the center of the buffer, not the top left corner.</remarks>
/// <param name="buffer">The buffer to which the body is rendered.</param>
/// <param name="renderName">Whether or not to render the name of the body.</param>
public void Render(ref RenderingBuffer buffer, bool renderName = false)
{
int roundedX = (int)Math.Round(buffer.BufferWidth / 2.0f + this.X);
int roundedY = (int)Math.Round(buffer.BufferHeight / 2.0f + this.Y);
buffer.SetPixel(roundedX, roundedY, new RenderingPixel(this.Character, this.ForegroundColor, this.BackgroundColor));
if(renderName)
{
buffer.SetString(roundedX + 1, roundedY, this.Name, this.BackgroundColor, ConsoleColor.Black);
}
}
}
}
BodySystem.cs
using System;
using System.Collections.Generic;
using CLIGL;
namespace GravitySimulator
{
/// <summary>
/// This class is responsible for simulating a system of bodies.
/// </summary>
public class BodySystem
{
public List<Body> Bodies { get; set; }
/// <summary>
/// Constructor for the BodySystem class.
/// </summary>
/// <param name="bodies">A list of bodies to simulate.</param>
public BodySystem(List<Body> bodies)
{
this.Bodies = bodies;
}
/// <summary>
/// Simulate the bodies.
/// </summary>
/// <param name="dt">The amount of time since the last frame, delta time.</param>
public void SimulateBodies(float dt)
{
foreach(Body body in this.Bodies)
{
body.IntegratePosition(dt);
}
foreach(Body otherBody in this.Bodies)
{
foreach(Body body in this.Bodies)
{
if(body.Name == otherBody.Name)
{
continue;
}
body.IntegrateVelocity(otherBody, dt);
}
}
}
/// <summary>
/// Render all the simulated bodies.
/// </summary>
/// <param name="buffer">The buffer to which to render.</param>
public void RenderBodies(ref RenderingBuffer buffer)
{
foreach(Body body in this.Bodies)
{
body.Render(ref buffer, true);
}
int offset = 1;
foreach(Body body in this.Bodies)
{
buffer.SetString(
0,
offset,
$"{body.Name} :: " +
$"P(x,y) = {body.X.ToString("F1")},{body.Y.ToString("F1")} :: " +
$"V(x,y) = {body.Vx.ToString("F1")},{body.Vy.ToString("F1")} :: " +
$"A(x,y) = {body.Ax.ToString("F2")},{body.Ay.ToString("F2")}",
ConsoleColor.White,
ConsoleColor.Black
);
offset++;
}
}
}
}
I am not as concerned with the correctness of my code prima facie (although suggestions for improvements in this regard would be appreciated nonetheless); rather, I am more concerned with the correctness of my implementation of the physics of gravitation, and any issues that may be present in it.
A few side notes:
- This project links a previous project of mine, CLIGL; if you wish to test out this simulator, you will need to download and compile CLIGL to a
.DLL
and link it accordingly. - I highly recommend that, should you wish to test this project for yourself, that you set the font of your console window to a raster font with a size of
12x16
. - For those who may not wish to go to the effort of downloading and compiling CLIGL and this project, I have uploaded two videos demonstrating the simulator: one without labels and one with labels.
- Finally, I am well aware that the value of the gravitational constant \$G\$ is \$6.67408\times10^{-11}\$ and not \$0.5\$, as I have implemented it here; using the correct value of \$G\$ results in a simulation that runs far too slow to appreciate.