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();
}
}
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.