How can I stop my endless runners background sprites from falling out of sync?

Hey folks!

So I’m doing an infinitely scrolling background using 4 copies of the same sprite scrolling to the left, teleporting a distance to the right off-screen if the sprite passes a specific distance to the left and then scrolling to the left again.

Unfortunately, after a few circulations of the background(s), the sprites start to separate from each other, or, alternately, overlap; their movement and reset fall out of sync. This is not good, I do not like this, can anybody divine a means by which I can enforce perfect sync on my background?

The functionality is handled in two scripts, reproduced below…

THE MOVER!

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// This component is used to move scenary and obstacles from the right of the screen to the left.

public class Mover : MonoBehaviour {
    // statePlay: so this is a bit of a mess. Ideally we'd drive an OnUpdate function from the
    // statePlay OnUpdate, but there are so many Movers switching from active to inactive and
    // back that it seemed to make more sense to just get a bool in statePlay.
    //     On mature reflection, should have driven from statePlay to the object pool to the
    // active movers. Ah well.
    private StatePlay statePlay;    

    public enum MoverType
    { // Related switch gets appropriate speed from GC.
        dummy,
        background,
        midground,
        obstacleAir,
        obstacleGnd,
        airHeli,
        airCloud,
        airBalloons,
        airHotBall,
        custom,
    }

    public  MoverType   moveType;
    [Header("Custom Speed only used")]
    [Header("with Custom Move Type.")] // Wish there was a way to auto-wrap this stuff. Custom inspector?
    public  float       customSpeed = 6.0f;

    private Rigidbody2D RB2D;

    public  Vector2     moveMeDirection = new Vector2(-1, 0);

    public  bool        goAlongSineWave = false;
    public  float       sineFrequency   = 20.0f;  // Speed of sine movement
    public  float       sineMagnitude   = 0.5f;   // Size of sine movement
    // private Vector3     axis            = Vector3.up;
    // private Vector3     pos;

    private void Awake()
    {
        RB2D = GetComponent<Rigidbody2D>();
    }

    // Use this for initialization
    void Start () {
        statePlay = GameObject.Find("TextScorePlay").GetComponent<StatePlay>();
	}
	
	// Update is called once per frame
	void Update () {

        float currentSpeedMultiplier = 1.0f;

        switch (moveType)
        { // Probably should have done this in START - when no longer tweaking speeds, move it back there!
            case MoverType.dummy:
                Debug.LogWarning("Move Type set to Dummy! Switching to Background...");
                moveType = MoverType.background;
                break;
            case MoverType.background:
                currentSpeedMultiplier = GC.Inst.moveSpd.backgrnd;
                break;
            case MoverType.midground:
                currentSpeedMultiplier = GC.Inst.moveSpd.midgnd;
                break;
            case MoverType.obstacleAir:
                currentSpeedMultiplier = GC.Inst.moveSpd.airDefault;
                break;
            case MoverType.obstacleGnd:
                currentSpeedMultiplier = GC.Inst.moveSpd.groundDefault;
                break;
            case MoverType.airHeli:
                currentSpeedMultiplier = GC.Inst.moveSpd.airHeli;
                break;
            case MoverType.airCloud:
                currentSpeedMultiplier = GC.Inst.moveSpd.airCloud;
                break;
            case MoverType.airBalloons:
                currentSpeedMultiplier = GC.Inst.moveSpd.airBalloons;
                break;
            case MoverType.airHotBall:
                currentSpeedMultiplier = GC.Inst.moveSpd.airHotBall;
                break;
            case MoverType.custom:
                currentSpeedMultiplier = customSpeed;
                break;
            default:

                break;
        }

        if (statePlay.amActive)
        {
            Vector2 moveOffset = moveMeDirection.normalized * GC.Inst.DTime() * currentSpeedMultiplier;

            if (goAlongSineWave)
            {
                Vector2 sineOffset = new Vector3(0.0f, Mathf.Sin(Time.time * sineFrequency) * sineMagnitude);
                RB2D.MovePosition(RB2D.position + sineOffset + moveOffset);
            }
            else
            {
                RB2D.MovePosition(RB2D.position + moveOffset);
            }
            
        }
        
	}
}

THE RESETTER!

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ResetMovement : MonoBehaviour {

    private Collider2D col;
    private Transform       tf;

    public float           groundHorizontalLength;

    private Vector2     startPos;

    private void Awake()
    {
        tf  = GetComponent<Transform>();
        startPos = tf.position;
        col = GetComponent<Collider2D>();
        groundHorizontalLength = col.bounds.size.x * 2;
    }

    // Use this for initialization
    void Start () {
		
	}
	
	// Update is called once per frame
	void Update () {
		if(tf.position.x < -groundHorizontalLength)
        {
            RepositionBackground();
        }
	}

    public void RepositionBackground ()
    {
        // tf.position = new Vector2(groundHorizontalLength, tf.position.y);

        Vector2 groundOffset = new Vector2(groundHorizontalLength * 2, 0);

        tf.position = (Vector2) tf.position + groundOffset;
    }

    public void StartingPositions ()
    {
        tf.position = startPos;
    }
}

Much appreciated for taking a look at this, and thank you in advance for any help you can give me.

–Rev

PS: I realise I could do a UV scroll as an alternative solution, but I’d like to find a specific answer to this issue if possible! Thank you!

So the solution was to move in FixedUpdate. Like always.

–RoB