Simulating the solar system #2

Simulating the solar system #2

S

Now that we have the basis set up to start simulating the solar system it is time to get into more interesting bits.
I’m going to write a quick updater that will update the positions of these bodies from their start one whenever the game is started. Very important here is to keep in mind that this updater will run in paralel in a different thread than the Unity one. This allows us to decuple the rendering step which will occur preferably more than sixty times a second from the physics step which for starters we’ll run thirty times a second.

Let’s start with the easy bit, write a class that we can instantiate with a given frequency, register a listener for whenever a given amount of time has passed and that will call our event listeners whenever that amount of time has passed. To achieve this I relied on C#’s Timer functionality, code below.

using System;
using System.Timers;

namespace HercDotTech.Tools
{
    public class Updater
    {
        private bool _isRunning;
        private readonly Timer _updateTimer;
        private long _lastTimestamp;

        public delegate void UpdateCallback(double deltaTime);
        public event UpdateCallback UpdateCallbacks;

        /// <summary>
        /// Builds a new updater with the specified frequency
        /// </summary>
        /// <param name="frequency"></param>
        public Updater(double frequency)
        {
            _updateTimer = new Timer
            {
                AutoReset = false,
                Interval = 1000d / frequency
            };

            _updateTimer.Elapsed += new ElapsedEventHandler((object o, ElapsedEventArgs e) => { Update(); });

            _isRunning = false;
        }

        public void Start()
        {
            if (!_isRunning)
            {               
                _isRunning = true;

                _lastTimestamp = UnixTimestamp.Now;
                Update();

                return;
            }

            throw new Exception("Updater already started!");
        }

        public void Stop()
        {
            if (!_isRunning)
                return;

            _updateTimer.Stop();
        }

        private void Update()
        {
            // Calculate the delta time between updates in seconds
            double deltaTime = (UnixTimestamp.Now - _lastTimestamp) / 1000d;
            _lastTimestamp = UnixTimestamp.Now;
            
            if (deltaTime > double.Epsilon)
                UpdateCallbacks.Invoke(deltaTime);

            _updateTimer.Start();
        }
    }
}

Given the large distances between solar systems I do not want celestial bodies from one solar system to interact with celestial bodies in another. Therefore, all the logic pertaining to updating the position of all the bodies in a solar system will be encapsulated in a solar system physics class. This will be exposing an update method (not to be confused with the update coming from Unity) that will first go through and calculate all the forces to be applied to each body then update all their positions accordingly.

using HercDotTech.Data;
using System;
using System.Collections.Generic;

namespace HercDotTech.Physics
{
    public static class SolarSystemPhysics
    {
        public static void Update(SolarSystem ss, double deltaTime)
        {
            foreach(Orbital target in ss.Orbitals)
            {
                if (target.Immovable == false)
                    target.AddForce(GetOrbitalForce(target, ss.Orbitals) * deltaTime);
            }

            foreach (Orbital target in ss.Orbitals)
            {
                if (target.Immovable == false)
                    target.UpdatePosition(deltaTime);
            }
        }

        private static DoubleVector3 GetOrbitalForce(Orbital target, List<Orbital> effectors)
        {
            DoubleVector3 force = DoubleVector3.zero;
            foreach(Orbital orb in effectors)
            {
                force += GetGravitationalPull(target, orb);
            }

            return force;
        }

        private static DoubleVector3 GetGravitationalPull(Orbital target, Orbital effector)
        {
            // Get a vector pointing from target to effector
            DoubleVector3 forceVector = effector.Position - target.Position;
            
            // The exact same position is a good equality check but not necesary
            // If the two bodies are closer than the PLANCK distance then we'll consider them as being in the same position and as
            // such essentially being the same body which means we want to disregard the attraction for this case
            if (forceVector.magnitude < Constants.PLANCK)
                return DoubleVector3.zero;

            // Return the vector's direction (normalized) times the new magnitude (gravitational attraction)
            return forceVector.normalized * Constants.GRAVITY * effector.Mass * target.Mass / Math.Pow(forceVector.magnitude, 2);
        }
    }
}

This updater will get called from a PhysicsManager class that is responsible for running the physics calculations in a separate thread to Unity’s engine. Essentially what this achieves is a complete decoupling in terms of execution time between our data layer and the renderer (Unity).
On top of being able to specify the frequency at which the physics should be updated at (30 Hz initially) we can also specify a time warp factor. What this will do is effecitvely speed up or slow down the movements of everything relying on this physics clock.

using HercDotTech.Data;
using UnityEngine;

namespace HercDotTech.GameObjects
{
    public class PhysicsManager : MonoBehaviour
    {
        [Range(1, 240)]
        public uint UpdateFrequency;
        public float TimeWarpFactor;

        private Tools.Updater _physicsUpdater;
        private SolarSystem[] _solarSystems;

        void Awake()
        {
            InitSolarSystems();

            _physicsUpdater = new Tools.Updater(UpdateFrequency);
            _physicsUpdater.UpdateCallbacks += UpdateSolarSystems;
        }

        void Start()
        {
            _physicsUpdater.Start();
        }

        private void InitSolarSystems()
        {
            SolarSystemManager[] ssms = GetComponentsInChildren<SolarSystemManager>();

            _solarSystems = new SolarSystem[ssms.Length];
            for (int i = 0; i < ssms.Length; i++)
            {
                ssms[i].Init();
                _solarSystems[i] = ssms[i].Data;
            }
        }

        private void UpdateSolarSystems(double deltaTime)
        {
            foreach(SolarSystem ss in _solarSystems)
            {
                Physics.SolarSystemPhysics.Update(ss, deltaTime * TimeWarpFactor);
            }
        }

        private void OnDestroy()
        {
            _physicsUpdater.Stop();
        }
    }
}

You might notice that instead of passing it a list of solar systems the manager finds all the children with a given component. I like having this doen that way because it simplifies object creation in Unity’s editor. In the end all of this is going to be read from a file so it doesn’t really matter but for now it saves having to input a lot of fields for every change.
You’ll see this pattern replicated in the code below whcih manages a solar system.

using HercDotTech.Data;
using UnityEngine;

namespace HercDotTech.GameObjects
{
    public class SolarSystemManager : MonoBehaviour
    {
        public string Name;

        public DoubleVector3 Position;

        public SolarSystem Data { get; private set; }

        public void Init()
        {
            Data = new SolarSystem(
                Name,
                Position
            );

            AddOrbitals();
        }

        private void AddOrbitals()
        {
            OrbitalManager[] oms = GetComponentsInChildren<OrbitalManager>();
 
            for (int i = 0; i < oms.Length; i++)
            {
                oms[i].Init();
                Data.AddOrbital(oms[i].Data);
            }
        }
    }
}

And that’s the basics done. With this, and the right values for radiuses, gravitational atracctions and positions we are able to simulate the whole thing. Don’t believe me, see below.

Next time we’ll handle even more interesting stuff. Being able to setup a full 5 to 6 planet solar system with a few moons and everything and building some helper tools because having to calculate by hand what all these values should be is a bit boring and complicated.