Search code examples
c#unity-game-enginetimersprite

Unity Can't flip sprite from timer elapsed function


I've just started exploring unity & c# working together, and accidently I faced a next problem: I have a fish. Fish was supposed to go from left to right, then change its direction and go from right to left. I haven't finished that moving part yet, but I was going to do it with timer. So timer is active, fish starts to move, timer stops, changing direction, timer resets, fish starts to move etc. I want to flip my sprite, so it will face correct direction. It doesn't work from Elapsed function and I don't understand why. If you have any ideas, please share

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


public class FishBehavior : MonoBehaviour
{
    public float moveSpeed;
    private int direction; //for left -1, for right 1
    private Timer timer;
    private SpriteRenderer img;
    private Rigidbody2D rb;


    void Start()
    {
        img = GetComponent<SpriteRenderer>();
        rb = GetComponent<Rigidbody2D>();
        direction = 1;
        timer = new Timer();
        timer.Elapsed += TimerElapsed;
        ChangeTimerOptions();
    }

    private void Move()
    {
        //moving
    }

    private void ChangeDirection()
    {
        direction *= -1;
        img.flipX = !img.flipX; //Doesn't work!
        //stop movement
    }

    private void ChangeTimerOptions()
    {
        System.Random rand = new System.Random();
        timer.Interval = rand.Next(3000, 8000);
        timer.Enabled = true;
        Move();
    }

    private void TimerElapsed(object source, ElapsedEventArgs e)
    {
        ChangeDirection();
        ChangeTimerOptions();
    }

}

Solution

  • The issue is that the callback for TimerElapsed is on a different thread than that of Unity. Meaning any sort of calls you make inside of it to methods that would normally work in Unity will not. I would recommend instead using a Coroutine or an Invoke.

    To briefly explain what both are, a Coroutine is a special method that completes small amounts of work over multiple frames. The method can also be suspended for certain amounts of time to wait to perform code down the line. An Invoke or InvokeRepeating is a call to perform a method after a certain amount of time or to continually make a call to a method after a set start time and set amount of time.

    If you are also eventually incorporating a movement and randomizing your wait time, I would consider using a Coroutine over an Invoke so you can handle all movement/flip logic from the same main call.

    public float moveSpeed;
    private int direction; //for left -1, for right 1
    private Timer timer;
    private SpriteRenderer img;
    private Rigidbody2D rb;
    
    // store the coroutine in case a duplicate call occurs somehow
    private Coroutine MoveAndFlip = null;
    
    void Start()
    {
        img = GetComponent<SpriteRenderer>();
        rb = GetComponent<Rigidbody2D>();
        direction = 1;
    
        // detect if any coroutine is already running
        CleanUpMoveAndFlipCoroutine();
    
        MoveAndFlip = StartCoroutine(MoveAndFlipAsset());
    }
    
    private void Move()
    {
        //moving
    }
    
    private void ChangeDirection()
    {
        direction *= -1;
        img.flipX = !img.flipX;
        //stop movement
    }
    
    private IEnumerator MoveAndFlipAsset()
    {
        // instead of milliseconds, use seconds
        float randomTimeInterval = Random.Range(3.0f, 8.0f);
    
        while(true)
        {
           // can do movement here - depending on how you would like to apply motion it would change how
           // it is implemented such as directly changing velocity, using a Vector3.Lerp, AddForce, etc.
    
            // wait the time to flip
            yield return new WaitForSeconds(randomTimeInterval);
    
            // now flip
            ChangeDirection();
        }
    }
    
    private void CleanUpMoveAndFlipCoroutine()
    {
        if (MoveAndFlip != null)
            StopCoroutine(MoveAndFlip);
    }
    

    If you would like an Invoke example to the above implementation, here is how you could approach it.

    void Start()
    {
        img = GetComponent<SpriteRenderer>();
        rb = GetComponent<Rigidbody2D>();
        direction = 1;
    
        Invoke("ChangeDirection", RandomTimeToFlip());
    }
    
    private float RandomTimeToFlip()
    {
        return Random.Range(3.0f, 8.0f); ;
    }
    
    private void ChangeDirection()
    {
        direction *= -1;
        img.flipX = !img.flipX;
        //stop movement
    
        // start our method again after a set amount of time
        Invoke("ChangeDirection", RandomTimeToFlip());
    }
    

    Edit: Apparently, you are able to use the TimerElapsed by changing the Timer's SynchronizingObject reference, but I am not exactly sure what to assign it to. I would still recommend using one of the two methods I described above though.