How would one model speed of light propagation in unity for a space combat sim?

I’m working on a space combat sim that takes place over relatively vast areas (a cube of space ~20 light minutes to a side) and uses (mostly) realistic physics. One of the key elements I want to model is the propagation speed of information across the battle space – that is, if Player A engages his main drive on one side of the battle space, there’s a 20 minute delay before Player B sees it and can react to it

Even for relatively small battle spaces this is an important factor given the speeds involved. A 500m long ship doing 30kps is going to displace itself by its full length in 1/60th of a second, so even targeting an opponent just few tenths of a light second away are going to be affected by light lag.

I’ve not seen this done before, and I’m wondering if there are some best practices for modeling this.

Similar to the excellent question asked here a few years ago on stack exchange (indeed I have taken his question as it is very well put), which is unresolved:
https://gamedev.stackexchange.com/questions/47784/modeling-speed-of-light-information-propagation-in-space-combat-sim

Are there any packages I should be aware of?

One way I had thought of doing it was to say have a sensor pule every x seconds emmited from the user object. And for objects away from the observer to emit a different layer every x seconds, which would become visible to the user when the “light” from the distant object has reached the user and then delete the layers as required. Although I think this would get intensive very quickly and only a few objects could be handled

Interesting question.

It strikes me as the easiest way to implement it would be to record transform history for every unit and then let any ship observe position of others by looking back into their history based on distance between them (to calc time delta). It’s not accurate but approximates the effect.

white paths - transform history
meshes - real positions
wire boxes - observed positions

note how the observed object position vary and depends on distance:
alt text

Zero optimizations, just a barely working, late-night sketch of an idea.

SpaceShip.cs

using System.Collections.Generic;
using UnityEngine;
  
public class SpaceShip : MonoBehaviour
{
    const float k_speed_of_light = 1f;// world units per second
    const int k_max_past = 100;// seconds to buffer
    [SerializeField] float _speed = 1f;// world units per second
    static List<SpaceShip> _instances = new List<SpaceShip>();
    RingBuffer<Matrix4x4> _past = new RingBuffer<Matrix4x4>( k_max_past );
  
    Vector3 _destination;
  
    #if UNITY_EDITOR
    void OnDrawGizmos ()
    {
        Vector3 position = transform.position;
        {
            Gizmos.color = Color.HSVToRGB( Mathf.Abs((float)GetHashCode()%17f)/17f , 1 , 1 );
            int numInstances = _instances.Count;
            for( int i=0 ; i<numInstances ; i++ )
            {
                var other = _instances[i];
                if( other!=this )
                {
                    float distance = Vector3.Magnitude( other.transform.position - position );
                    float seconds = distance / k_speed_of_light;
                    var enumerator = other._past.GetEnumeratorBackward();
                    int secondsInt = Mathf.FloorToInt( seconds );
                    for( int step=0 ; step<=secondsInt ; step++ )
                        enumerator.MoveNext();
                    Gizmos.DrawLine( position , enumerator.Current.GetColumn(3) );
                    Gizmos.matrix = enumerator.Current;//Matrix4x4.TRS( pos , enumerator.Current.rotation , Vector3.one );
                    Gizmos.DrawWireCube( Vector3.zero , Vector3.one/2f );
                    Gizmos.matrix = Matrix4x4.identity;
                }
            }
        }
        {
            var enumerator = _past.GetEnumeratorBackward();
            enumerator.MoveNext();
            int i = 0;
            Vector3 p0 = enumerator.Current.GetColumn(3);
            while( enumerator.MoveNext() )
            {
                Vector3 p1 = enumerator.Current.GetColumn(3);
                Gizmos.color = new Color{ r=1 , g=1 , b=1 , a=1f-((float)(i++)/(float)k_max_past) };
                Gizmos.DrawLine( p0 , p1 );
                p0 = p1;
            }
        }
    }
    #endif
  
    void OnEnable () => _instances.Add( this );
    void OnDisable () => _instances.Remove( this );
  
    void Start ()
    {
        var ltw = transform.localToWorldMatrix;
        for( int i=_past.Capacity-1 ; i!=-1 ; i-- )
            _past.Push( ltw );
        
        InvokeRepeating( nameof(Tick) , 0f , 1f );
        InvokeRepeating( nameof(RandomDestination) , 0f , 1.77f );
    }
  
    void FixedUpdate ()
    {
        float dt = Time.fixedDeltaTime;
        Vector3 pos = transform.position;
        Vector3 dir = _destination - pos;
        transform.position = Vector3.MoveTowards( pos , _destination , _speed * dt );
        transform.rotation = Quaternion.Lerp( transform.rotation , Quaternion.LookRotation(dir.normalized) , dt );
    }
  
    void Tick () => _past.Push( transform.localToWorldMatrix );
  
    void RandomDestination () => _destination = Random.insideUnitSphere * 10f;
  
}

RingBuffer.cs

using System.Collections;
using System.Collections.Generic;
[System.Serializable]
public class RingBuffer <T> : IEnumerable, IEnumerable<T>
{
	readonly T[] _array;
	public readonly int Capacity;
		
	/// <summary> Current index </summary>
	/// <remarks> (not next one) </remarks>
	int _index;
	public int Index => _index;
	  
	[System.Obsolete("don't",true)]
	public RingBuffer () {}
	public RingBuffer ( int capacity )
	{
		this._array = new T[ capacity ];
		this.Capacity = capacity;
		this._index = 0;
	}
		
	public void Push ( T value )
	{
		_array[ _index++ ] = value;
		if( _index==Capacity ) _index = 0;
	}
	public T Peek () => _array[_index];
	public T this [ int index ] => _array[index];
	  
	IEnumerator IEnumerable.GetEnumerator ()
	{
		for( int i=_index ; i<Capacity ; i++ ) yield return _array[i];
		for( int i=0 ; i<_index ; i++ ) yield return _array[i];
	}
	public IEnumerator<T> GetEnumerator ()
	{
		for( int i=_index ; i<Capacity ; i++ ) yield return _array[i];
		for( int i=0 ; i<_index ; i++ ) yield return _array[i];
	}
	public IEnumerator<T> GetEnumeratorBackward ()
	{
		for( int i=_index ; i!=-1 ; i-- ) yield return _array[i];
		for( int i=Capacity-1 ; i>_index ; i-- ) yield return _array[i];
	}
	  
	public T[] AsArray () => _array;
  
}

create an gameobject to represent the ships real positioning. Then the model of the ship goes under it.

Using Vector3.distance() you should be able to figure out how close that ship is to you. You can do a quick calculation of ingame distance to light speed minutes/seconds (NOTICE: unless the player has a lot to do, even a single minute is a lot of time to be uninformed. OR ingame seconds do not equal out of game seconds).

As the gameobject moves, have the model “ghost” the gameobject, following it within a certain amount of time. Example: gameobject moves, turns left, moves. Ship moves seconds more, turns left (20 seconds after gameobject turned), moves (again later). Basically the animation would be set backward a number of seconds. Changing the animation speed to catch up (ships closer to player) or slow down (ships further than player).

This solutions DOES NOT WORK on multiplayer, at least without a server only sending the models through.