I'm trying to create a wave spawner for a top down game that I'm creating. I have created the wave spawner script but when I hit play, nothing happens. The countdown doesn't begin. Ideally it should start from 2, once it reaches 0, the first wave should spawn with one enemy. Once that enemy is killed, the countdown should begin from 5 and once 0 is reached, the next wave with 2 enemies should begin and so on. The new wave should not begin until all the current enemies are destroyed.
public enum SpawnState { SPAWNING, WAITING, COUNTING };
public SpawnState state = SpawnState.COUNTING;
public Transform enemy;
public float timeBetweenWaves = 5f;
public float countDown = 2f;
private int waveIndex = 0;
public float searchCountdown = 1f;
void Update()
{
if (state == SpawnState.WAITING)
{
if (!EnemyisAlive())
{
WaveCompleted();
}
else
{
return;
}
}
if (countDown <= 0f)
{
if (state != SpawnState.SPAWNING)
{
StartCoroutine(SpawnWave());
countDown = timeBetweenWaves;
}
else
{
countDown -= Time.deltaTime;
}
}
}
void WaveCompleted()
{
state = SpawnState.COUNTING;
countDown = timeBetweenWaves;
SpawnWave();
}
bool EnemyisAlive()
{
searchCountdown -= Time.deltaTime;
if (searchCountdown <= 0)
{
searchCountdown = 1f;
if (GameObject.FindGameObjectsWithTag("Enemy").Length == 0)
{
return false;
}
}
return true;
}
IEnumerator SpawnWave()
{
state = SpawnState.SPAWNING;
waveIndex++;
for (int i = 0; i < waveIndex; i++)
{
SpawnEnemy();
yield return new WaitForSeconds(0.5f);
}
state = SpawnState.WAITING;
yield break;
}
void SpawnEnemy()
{
Instantiate(enemy, transform.position, transform.rotation);
}
I would recommend you to use a Coroutine for all of it. That makes some things easier. You can e.g. simply wait until another Ienumertaor is finished. Then I would simply add the spawned enemies to a list, filter it for null entries and use the count. Using Find or in your case FindGameObjectsWithTag each frame is highly inefficient!
using System.Linq;
using System.Collections.Generic;
...
public Transform enemy;
public float timeBetweenWaves = 5f;
public float countDown = 2f;
//public float searchCountdown = 1f;
private List<Transform> enemies = new List<Transform>();
private int waveIndex = 0;
private void Start()
{
StartCoroutine(RunSpawner());
}
// this replaces your Update method
private IEnumerator RunSpawner()
{
// first time wait 2 seconds
yield return new WaitForSeconds(countDown);
// run this routine infinite
while(true)
{
state = SpawnState.SPAWNING;
// do the spawning and at the same time wait until it's finished
yield return SpawnWave();
state = SpawnState.WAITING;
// wait until all enemies died (are destroyed)
yield return new WaitWhile(EnemyisAlive);
state = SpawnState.COUNTING
// wait 5 seconds
yield return new WaitForSeconds(timeBetweenWaves);
}
}
private bool EnemyisAlive()
{
// uses Linq to filter out null (previously detroyed) entries
enemies = enemies.Where(e => e != null).ToList();
return enemies.Count > 0;
}
private IEnumerator SpawnWave()
{
waveIndex++;
for (int i = 0; i < waveIndex; i++)
{
SpawnEnemy();
yield return new WaitForSeconds(0.5f);
}
}
private void SpawnEnemy()
{
enemies.Add(Instantiate(enemy, transform.position, transform.rotation));
}
To be slightly more efficient you could also avoid instantiating and destroying but rather use Object Pooling - only enabling and disabling the objects and eventually spawn new ones only when needed.
Related
This question already has answers here:
How to make the script wait/sleep in a simple way in unity
(7 answers)
Closed 1 year ago.
This is the waypoints manager script attached to empty GameObject :
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
public class WaypointsManager : MonoBehaviour
{
public GameObject npcPrefab;
public int numberOfNpcs;
public GameObject waypointsPrefab;
public List<GameObject> waypoints = new List<GameObject>();
public int numberOfWaypoints;
public bool useWaypointsPrefab = false;
private GameObject waypointObject;
// Start is called before the first frame update
void Awake()
{
for (int i = 0; i < numberOfWaypoints; i++)
{
if (useWaypointsPrefab)
{
waypointObject = Instantiate(npcPrefab, Vector3.zero, Quaternion.identity);
}
else
{
waypointObject = new GameObject();
}
waypointObject.tag = "Waypoint";
waypointObject.name = "Waypoint";
waypointObject.transform.position = new Vector3(Random.Range(0, 10), Random.Range(0, 10), Random.Range(0, 10));
waypoints.Add(waypointObject);
}
for (int i = 0; i < numberOfNpcs; i++)
{
if (npcPrefab != null)
{
GameObject npc = Instantiate(npcPrefab, Vector3.zero, Quaternion.identity);
}
}
}
// Update is called once per frame
void Update()
{
}
}
And this script is attached to each npc :
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
public class Waypoints : MonoBehaviour
{
public List<GameObject> waypoints = new List<GameObject>();
public float movementSpeed;
public float rotationSpeed;
public bool reverse = false;
public bool go = false;
public int numberOfWaypoints;
public int nextWaypointNumber;
private int waypointIndex = 0;
private GameObject nextWayPoint;
// Start is called before the first frame update
void Start()
{
waypoints = GameObject.FindGameObjectsWithTag("Waypoint").ToList();
numberOfWaypoints = waypoints.Count;
if (reverse)
{
waypointIndex = waypoints.Count - 1;
}
else
{
waypointIndex = 0;
}
StartCoroutine(MoveNpc());
}
// Update is called once per frame
void Update()
{
if (go)
{
if (reverse && waypointIndex == 0)
{
waypointIndex = waypoints.Count - 1;
}
if (reverse == false && waypointIndex == waypoints.Count)
{
waypointIndex = 0;
}
nextWayPoint = waypoints[waypointIndex];
nextWaypointNumber = waypointIndex;
transform.position = Vector3.MoveTowards(transform.position,
waypoints[waypointIndex].transform.position, Time.deltaTime * movementSpeed);
float distance = Vector3.Distance(transform.position, waypoints[waypointIndex].transform.position);
if (distance > 0f)
{
// Try to rotate to face the waypoint only if we're not on top of it.
var rotation = Quaternion.LookRotation(nextWayPoint.transform.position - transform.position);
transform.rotation = Quaternion.Slerp(transform.rotation, rotation, Time.deltaTime * rotationSpeed);
}
else
{
numberOfWaypoints--;
if (reverse)
{
waypointIndex--;
}
else
{
waypointIndex++;
}
}
}
}
private IEnumerator MoveNpc()
{
yield return new WaitForSeconds(3f);
go = true;
}
private void OnDrawGizmos()
{
if (waypoints != null)
{
for (int i = 0; i < waypoints.Count; i++)
{
Gizmos.color = Color.green;
Gizmos.DrawSphere(waypoints[i].transform.position, 0.1f);
}
}
if (nextWayPoint != null)
{
Gizmos.color = Color.red;
Gizmos.DrawLine(transform.position, nextWayPoint.transform.position);
}
}
}
In the Waypoints script I'm starting a coroutine with a 3 seconds delay but still all the npcs are moving at the same time like one npc. I want it to wait 3 seconds send npc wait 3 seconds send npc until all npcs are moving along the waypoints.
StartCoroutine(MoveNpc());
ISSUE
All npcs are being spawned at the same time, each waits 3 seconds (all at the same time), and then start moving.
SOLUTION
You could create a function that takes the amount of time they need to wait, and use that delay time in your coroutine.
NPC
public void StartMovingAfterSeconds(float seconds)
{
StartCoroutine(MoveNPC(seconds));
}
private IEnumerator MoveNpc(float delayTimeSeconds)
{
yield return new WaitForSeconds(delayTimeSeconds);
go = true;
}
From your manager you will need to track the delay time as you spawn the npcs.
MANAGER
// Amount to delay movement by (can be exposed in the inspector)
//
var delayTime = 3f;
// Accumulated delay
//
var currentDelay = 0f;
if (npcPrefab != null)
{
for (int i = 0; i < numberOfNpcs; i++)
{
var npc = Instantiate(npcPrefab, Vector3.zero, Quaternion.identity);
npc.StartMovingAfterSeconds(currentDelay);
currentDelay += delayTime;
}
}
We start with a delay of 0 (currentDelay) and add the amount we want to delay by (delayTime) in each iteration. The first delay is 0, next is 3, then 6, etc..
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GenerateWalls : MonoBehaviour
{
public GameObject gameObjectToRaise;
public float duration;
public Vector3 raiseAmount;
public bool go = false;
public Color[] colors = new Color[4];
public bool randomColors = false;
private GameObject objtoraise;
private GameObject[] walls;
private bool scaleOver = false;
private void Start()
{
Init();
ColorWalls();
// The z Axis must be minimum 1 or any value above 0 could be also 0.1f
// but it's better to keep it minimum as 1 by default.
if (raiseAmount.z < 1)
{
raiseAmount.z = 1f;
}
if (go)
{
StartCoroutine(ScaleOverSeconds(objtoraise, new Vector3(raiseAmount.x, raiseAmount.y,
raiseAmount.z), duration));
}
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.R))
{
//if (scaleOver)
//{
if (objtoraise != null)
{
if (raiseAmount.z < 1)
{
raiseAmount.z = 1f;
}
Destroy(objtoraise);
Init();
ColorWalls();
StartCoroutine(ScaleOverSeconds(objtoraise, new Vector3(raiseAmount.x, raiseAmount.y,
raiseAmount.z), duration));
scaleOver = false;
//}
}
}
}
private void Init()
{
objtoraise = Instantiate(gameObjectToRaise);
objtoraise.name = "Walls";
walls = GameObject.FindGameObjectsWithTag("Wall");
}
public IEnumerator ScaleOverSeconds(GameObject objectToScale, Vector3 scaleTo, float seconds)
{
float elapsedTime = 0;
Vector3 startingScale = objectToScale.transform.localScale;
while (elapsedTime < seconds)
{
objectToScale.transform.localScale = Vector3.Lerp(startingScale, scaleTo, (elapsedTime / seconds));
elapsedTime += Time.deltaTime;
yield return new WaitForEndOfFrame();
}
objectToScale.transform.localScale = scaleTo;
scaleOver = true;
}
private void ColorWalls()
{
for (int i = 0; i < walls.Length; i++)
{
if (randomColors)
{
walls[i].transform.GetChild(0).GetComponent<Renderer>().material.color
= GetRandomColour32();
}
else
{
walls[i].transform.GetChild(0).GetComponent<Renderer>().material.color = colors[i];
}
}
}
private Color32 GetRandomColour32()
{
//using Color32
return new Color32(
(byte)UnityEngine.Random.Range(0, 255), //Red
(byte)UnityEngine.Random.Range(0, 255), //Green
(byte)UnityEngine.Random.Range(0, 255), //Blue
255 //Alpha (transparency)
);
}
}
Inside the Update() when I press the R key it's destroying the Instantiated object and then Instantiate is again and start the coroutine again. The problem is when I press on the R key many times in a row after two times I'm getting MissingReferenceException exception in the editor :
MissingReferenceException: The object of type 'GameObject' has been destroyed but you are still trying to access it.
Your script should either check if it is null or you should not destroy the object.
GenerateWalls+d__12.MoveNext () (at Assets/Scripts/GenerateWalls.cs:81)
Line 81 is :
objectToScale.transform.localScale = Vector3.Lerp(startingScale, scaleTo, (elapsedTime / seconds));
The goal is to be able to generate the walls each time over again when pressing R it should stop the current coroutine and start over.
Maybe the problem is that it's in the middle of the coroutine and because the old coroutine didn't stop yet then the object is missing in the middle because I destroy it?
How then I should do it to be able to press R over and over again and it will start the coroutine over and over? Not to start multiple coroutines but to start each time the coroutine over again.
The solution is to add : StopCoroutine In the Update()
This is not the solution. I thought stopping the coroutine will stop also the while loop inside but it didn't. It seems that checking for null inside the while loop solved the problem :
public IEnumerator ScaleOverSeconds(GameObject objectToScale, Vector3 scaleTo, float seconds)
{
if (objectToScale != null)
{
float elapsedTime = 0;
Vector3 startingScale = objectToScale.transform.localScale;
while (elapsedTime < seconds)
{
if (objectToScale == null)
{
yield return null;
}
else
{
objectToScale.transform.localScale = Vector3.Lerp(startingScale, scaleTo, (elapsedTime / seconds));
elapsedTime += Time.deltaTime;
yield return new WaitForEndOfFrame();
}
}
objectToScale.transform.localScale = scaleTo;
scaleOver = true;
}
}
The post is a bit long but both scripts are connected to each other. I tried to reduce the amount of code.
The Waypoints script is attached to empty GameObject and I added to it a rotation part :
[Header("Rotation")]
public Quaternion rotationTothinkWhatToDoHere;
but I'm not sure how to use it here in the Waypoints script and in the WaypointsFollower script.
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Cinemachine;
public class Waypoints : MonoBehaviour
{
[Header("Objects To Move")]
public Transform objectToMovePrefab;
public int numberOfObjectsToMove = 1;
public bool moveInReverse = false;
[Header("Speed")]
public float speed;
public bool randomSpeed = false;
public float minRandomSpeed = 1;
public float maxRandomSpeed = 100;
private bool changeSpeedOnce = false;
[Header("Rotation")]
public Quaternion rotationTothinkWhatToDoHere;
[Header("Waypoints")]
[SerializeField] private List<Transform> waypoints;
public bool moveOnWaypoints = false;
[Header("Delay")]
public bool useDelay = false;
public float delay = 3;
public bool randomDelay = false;
public float minRandomDelay = 0.3f;
public float maxRandomDelay = 5;
[Header("LineRenderer")]
public LineRenderer lineRenderer;
public bool moveOnLineRenderer = false;
public List<Vector3> lineRendererPositions = new List<Vector3>();
[Header("Cinemachine Cameras")]
public CinemachineVirtualCamera virtualCamera;
private List<WaypointsFollower> waypointsFollowers = new List<WaypointsFollower>();
private void Start()
{
for (int i = 0; i < numberOfObjectsToMove; i++)
{
var parent = GameObject.Find("Moving Object Parent");
var objectToMove = Instantiate(objectToMovePrefab, parent.transform);
objectToMove.name = "Platfrom";
waypointsFollowers.Add(objectToMove.GetComponent<WaypointsFollower>());
}
virtualCamera.Follow = waypointsFollowers[0].gameObject.transform;
virtualCamera.LookAt = waypointsFollowers[0].gameObject.transform;
foreach (WaypointsFollower wpf in waypointsFollowers)
{
wpf.goForward = moveInReverse;
}
WaypointsMovementStates();
SpeedUpdater();
if (useDelay)
StartCoroutine(SendObjectstomoveWithDelay());
}
private void Update()
{
lineRendererPositions.Clear();
lineRendererPositions.AddRange(GetLinePointsInWorldSpace());
SpeedUpdater();
}
IEnumerator SendObjectstomoveWithDelay()
{
{
foreach (WaypointsFollower follower in waypointsFollowers)
{
if (randomDelay)
{
delay = Random.Range(minRandomDelay, maxRandomDelay);
}
yield return new WaitForSeconds(delay);
follower.go = true;
}
}
}
private void SpeedUpdater()
{
if (changeSpeedOnce == false)
{
foreach (WaypointsFollower follower in waypointsFollowers)
{
if (randomSpeed)
{
follower.speed = Random.Range(minRandomSpeed, maxRandomSpeed);
}
else
{
follower.speed = speed;
}
}
changeSpeedOnce = true;
}
}
Vector3[] GetLinePointsInWorldSpace()
{
var positions = new Vector3[lineRenderer.positionCount];
//Get the positions which are shown in the inspector
lineRenderer.GetPositions(positions);
//the points returned are in world space
return positions;
}
private void WaypointsMovementStates()
{
// If moving on both linerenderer positions and waypoints objects
if (moveOnLineRenderer && moveOnWaypoints && waypoints.Count > 0)
{
if (useDelay == false)
{
foreach (WaypointsFollower wpf in waypointsFollowers)
{
wpf.go = true;
}
}
}
// If moving on linerenderer positions only without moving on waypoints objects
if (moveOnLineRenderer && moveOnWaypoints == false)
{
if (waypoints.Count > 0)
waypoints.Clear();
if (useDelay == false)
{
foreach (WaypointsFollower wpf in waypointsFollowers)
{
wpf.go = true;
}
}
}
// If only to move on waypoints objects without moving on linerenderer positions
if (moveOnWaypoints && waypoints.Count > 0 && moveOnLineRenderer == false)
{
lineRendererPositions.Clear();
foreach (Transform wp in waypoints)
{
lineRendererPositions.Add(wp.position);
}
if (useDelay == false)
{
foreach (WaypointsFollower wpf in waypointsFollowers)
{
wpf.go = true;
}
}
}
if(moveInReverse)
{
foreach (WaypointsFollower wpf in waypointsFollowers)
{
wpf.go = true;
}
}
}
}
In the WaypointsFollower script, this script is attached to each object that moves along the waypoints.
It's working fine if goForward is true but when goFoward is false for some reason the index value is -1 and I'm getting exception out of index was out of range on line 73 :
newPos = Vector3.MoveTowards(oldPos, waypoints.lineRendererPositions[index], distanceToTravel);
The idea when goForward is false to move the object from the last waypoint to the first waypoint and then when it's reaching the first waypoint then switch the goForward to true and move forward from the first waypoint to the last.
It's working when goFoward is first time true then it's moving from the first waypoint to the last waypoint and then it's moving from the last waypoint to the first one but it's not working when goForward is first time false.
I can't figure out why it's -1
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
public class WaypointsFollower : MonoBehaviour
{
public float speed;
public Waypoints waypoints;
public bool go;
public bool goForward;
private int index = 0;
private int counter = 0;
private int c = 0;
private List<GameObject> curvedLinePoints = new List<GameObject>();
public int numofposbetweenpoints;
private bool getonce;
private void Start()
{
waypoints = GameObject.Find("Waypoints").GetComponent<Waypoints>();
curvedLinePoints = GameObject.FindGameObjectsWithTag("Curved Line Point").ToList();
if(waypoints.moveInReverse == false)
{
goForward = true;
}
else
{
goForward = false;
}
if(goForward)
{
index = 0;
}
else
{
index = waypoints.lineRendererPositions.Count - 1;
}
}
private void Update()
{
if (getonce == false)
{
numofposbetweenpoints = curvedLinePoints.Count;
getonce = true;
}
if (go == true && waypoints.lineRendererPositions.Count > 0)
{
Move();
}
}
private void Move()
{
Vector3 newPos = transform.position;
float distanceToTravel = speed * Time.deltaTime;
bool stillTraveling = true;
while (stillTraveling)
{
Vector3 oldPos = newPos;
// error exception out of bound on line 55 to check !!!!!
newPos = Vector3.MoveTowards(oldPos, waypoints.lineRendererPositions[index], distanceToTravel);
distanceToTravel -= Vector3.Distance(newPos, oldPos);
if (newPos == waypoints.lineRendererPositions[index]) // Vector3 comparison is approximate so this is ok
{
// when you hit a waypoint:
if (goForward)
{
bool atLastOne = index >= waypoints.lineRendererPositions.Count - 1;
if (!atLastOne)
{
index++;
counter++;
if (counter == numofposbetweenpoints)
{
c++;
counter = 0;
}
if (c == curvedLinePoints.Count - 1)
{
c = 0;
}
}
else { index--; goForward = false; }
}
else
{ // going backwards:
bool atFirstOne = index <= 0;
if (!atFirstOne)
{
index--;
counter++;
if (counter == numofposbetweenpoints)
{
c++;
counter = 0;
}
if (c == curvedLinePoints.Count - 1)
{
c = 0;
}
}
else { index++; goForward = true; }
}
}
else
{
stillTraveling = false;
}
}
transform.position = newPos;
}
}
I saw your previous post before it was deleted, so here is the answer I had for your original question of moving between waypoints by both rotating and movement with the option of what occurs at the end of the motion. I can answer your current question if answering your last deleted question has still not solved your issue.
Instead of using the Update function to handle the rotation and movement between a series of waypoints in a list, I would recommend using a Coroutine. If you are unfamiliar, think of them as a process that handles small increments of work overtime and will jump back where it leaves off. It should simplify the issue of rotation and movement into smaller bite-sized pieces of logic, allowing an easier time to understand your issue.
// new enum - outside of your class
public enum WaypointMovementType
{
REPEAT_START, // will repeat to the start waypoint when end is reached
REPEAT_REVERSE, // will reverse the waypoint list when end is reached
STOP // will terminate when end is reached
};
// new variables - this is inside your class
// keep a reference of our coroutine to not run duplicates
Coroutine movingWayPoints = null;
// time it takes to rotate to our goal waypoint
private float rotateTime = 0.5f;
// time it takes to move to our goal waypoint
private float movementTime = 1.5f;
// Start is called before the first frame update
void Start()
{
parent = GameObject.Find("Waypoints");
// generate the waypoints
GenerateWaypoints();
// run our process
if (movingWayPoints == null)
movingWayPoints = StartCoroutine(MoveBetweenWaypoints(tmpList, WaypointMovementType.STOP));
}
private IEnumerator MoveBetweenWaypoints(List<Vector3> waypoints, WaypointMovementType movementType)
{
int currentWaypointIdx = 0;
// continue our loop until we have reached our end goal waypoint
while (currentWaypointIdx < waypoints.Count)
{
// rotate towards out goal point
yield return StartCoroutine(RotateTowardsGoalWaypoint(waypoints[currentWaypointIdx]));
// move towards our goal point
yield return StartCoroutine(MoveTowardsGoalWaypoint(waypoints[currentWaypointIdx]));
// increment our index count or wait for further time if you would like a delay between rotation / movement
++currentWaypointIdx;
}
// coroutine is done, so set the motion to null
movingWayPoints = null;
// now that we have reached the end, determine what we want to do
if (movementType != WaypointMovementType.STOP)
{
// if we want to reverse, then reverse our list
if (movementType == WaypointMovementType.REPEAT_REVERSE)
waypoints.Reverse();
// now call the coroutine again
movingWayPoints = StartCoroutine(MoveBetweenWaypoints(waypoints, movementType));
}
}
private IEnumerator RotateTowardsGoalWaypoint(Vector3 goalWaypoint)
{
// store our current rotation
Quaternion initialRotation = transform.rotation;
// find our direction to the goal
Vector3 dir = goalWaypoint - transform.position;
// calculate the final / goal rotation
Quaternion finalRotation = Quaternion.LookRotation(dir);
// store our current time
float currentTime = 0.0f;
// rotate until we reach our goal
while (currentTime <= rotateTime)
{
currentTime += Time.deltaTime;
transform.rotation = Quaternion.Lerp(initialRotation, finalRotation, currentTime / rotateTime);
yield return null;
}
// set our rotation in case there are floating point precision errors
transform.rotation = finalRotation;
}
private IEnumerator MoveTowardsGoalWaypoint(Vector3 goalWaypoint)
{
// store our current position
Vector3 initialPostion = transform.position;
// store our current time
float currentTime = 0.0f;
while (currentTime <= movementTime)
{
currentTime += Time.deltaTime;
transform.position = Vector3.Lerp(initialPostion, goalWaypoint, currentTime / movementTime);
yield return null;
}
// set our position in case there are floating point precision errors
transform.position = goalWaypoint;
}
The current code uses time instead of speed which can be changed by changing how the Lerp steps are inputted. One other issue currently is the repeat backwards will move and rotate to the first element in the reversed list, but this can be fixed by passing in an index parameter for where to start in the list.
If you would rather use your current implementation, I can help debug it, but in the future do not delete questions you expect an answer to unless you have a good reason to do so. I should also mention, as your original question had only 1 script, the current script is expecting the object that generates the waypoints is the same object that is moving between them. It would be very easy to fix by creating a new public field that references some other Transform that should move between the waypoints, then replace all of the transform.rotation and transform.position with yourObject.position and yourobject.rotation.
I should also add, to change how the waypoint movement will function after a single pass is finished, simply change the WaypointMovementType parameter to a different value before making the initial call.
Edit: As derHugo mentioned that a speed variant of the answer would better suit the needs of the use case, here is an additional snippet with speed instead of time.
private IEnumerator MoveBetweenWaypoints(List<Vector3> waypoints, WaypointMovementType movementType)
{
int currentWaypointIdx = 0;
// continue our loop until we have reached our end goal waypoint
while (currentWaypointIdx < waypoints.Count)
{
// rotate towards out goal point
yield return StartCoroutine(RotateTowardsGoalWaypoint(waypoints[currentWaypointIdx]));
// move towards our goal point
yield return StartCoroutine(MoveTowardsGoalWaypoint(waypoints[currentWaypointIdx]));
// increment our index count or wait for further time if you would like a delay between rotation / movement
++currentWaypointIdx;
}
// coroutine is done, so set the motion to null
movingWayPoints = null;
// now that we have reached the end, determine what we want to do
if (movementType != WaypointMovementType.STOP)
{
// if we want to reverse, then reverse our list
if (movementType == WaypointMovementType.REPEAT_REVERSE)
waypoints.Reverse();
// now call the coroutine again
movingWayPoints = StartCoroutine(MoveBetweenWaypoints(waypoints, movementType));
}
}
private IEnumerator RotateTowardsGoalWaypoint(Vector3 goalWaypoint)
{
// find our direction to the goal
Vector3 dir = goalWaypoint - transform.position;
// calculate the final / goal rotation
Quaternion finalRotation = Quaternion.LookRotation(dir);
// continue until our angles match
while(Vector3.Angle(transform.forward, dir) > ROTATION_CHECK_EPSILON)
{
transform.rotation = Quaternion.RotateTowards(transform.rotation, finalRotation, Time.deltaTime * rotateSpeed);
yield return null;
}
// set our rotation in case there are floating point precision errors
transform.rotation = finalRotation;
}
private IEnumerator MoveTowardsGoalWaypoint(Vector3 goalWaypoint)
{
// continue until our distance is close to our goal
while(Vector3.Distance(transform.position, goalWaypoint) > DISTANCE_CHECK_EPSILON)
{
transform.position = Vector3.MoveTowards(transform.position, goalWaypoint, Time.deltaTime * movementSpeed);
yield return null;
}
// set our position in case there are floating point precision errors
transform.position = goalWaypoint;
}
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
public class MoveOnCurvedLines : MonoBehaviour
{
public LineRenderer lineRenderer;
public List<GameObject> objectsToMove = new List<GameObject>();
public float speed;
public bool go = false;
public bool moveToFirstPositionOnStart = false;
private Vector3[] positions;
private Vector3[] pos;
private int index = 0;
private bool goForward = true;
private List<GameObject> objectsToMoveCopy = new List<GameObject>();
// Start is called before the first frame update
void Start()
{
objectsToMove = GameObject.FindGameObjectsWithTag("New Prefab").ToList();
pos = GetLinePointsInWorldSpace();
if (moveToFirstPositionOnStart == true)
{
for (int i = 0; i < objectsToMove.Count; i++)
{
objectsToMove[i].transform.position = pos[index];
}
}
StartCoroutine(AddNew());
}
Vector3[] GetLinePointsInWorldSpace()
{
positions = new Vector3[lineRenderer.positionCount];
//Get the positions which are shown in the inspector
lineRenderer.GetPositions(positions);
//the points returned are in world space
return positions;
}
// Update is called once per frame
void Update()
{
if (go == true)
{
Move();
}
}
void Move()
{
for (int i = 0; i < objectsToMoveCopy.Count; i++)
{
Vector3 newPos = objectsToMoveCopy[i].transform.position;
float distanceToTravel = speed * Time.deltaTime;
bool stillTraveling = true;
while (stillTraveling)
{
Vector3 oldPos = newPos;
newPos = Vector3.MoveTowards(oldPos, pos[index], distanceToTravel);
distanceToTravel -= Vector3.Distance(newPos, oldPos);
if (newPos == pos[index]) // Vector3 comparison is approximate so this is ok
{
// when you hit a waypoint:
if (goForward)
{
bool atLastOne = index >= pos.Length - 1;
if (!atLastOne) index++;
else { index--; goForward = false; }
}
else
{ // going backwards:
bool atFirstOne = index <= 0;
if (!atFirstOne) index--;
else { index++; goForward = true; }
}
}
else
{
stillTraveling = false;
}
}
objectsToMoveCopy[i].transform.position = newPos;
}
}
IEnumerator AddNew()
{
WaitForSeconds waitThreeSeconds = new WaitForSeconds(3);
foreach (var objToMove in objectsToMove)
{
yield return waitThreeSeconds;
objectsToMoveCopy.Add(objToMove);
}
}
}
I'm using StartCoroutine and the method AddNew to move each object between the waypoints every 3 seconds.
The logic :
First object to move from the List is start moving from the first position.
After 3 seconds the second object to move from the List is start moving from the first position.
The goal is to make that each object will start moving from the first position after 3 seconds following the first moving object before him so in the end I will have the objects moving with spoaces of 3 seconds between them.
The problem :
The first object is start moving after 3 seconds from the first position then the second and third and the resto f objects are start moving but from the last moved object and the other objects that already move are get merged with the other objects in the end I have a group of all the objects to move are moving together.
The same behave I want to be if they are moving in reverse.
Your objects merge, since you only have one index for the objects' target position, meaning all objects move towards the same point, not each object towards it's respective next point on the path. This results in a merge as soon as the first object turns around and runs backwards.
It would be best to split your logic into two classes, since otherwise you'd have to keep track of every object's path separately, meaning you need an int[] indices for the current target position of each object, another array for the goForward bools and so on for every new property you introduce.
Controller:
public class MovementController : MonoBehaviour
{
[SerializeField]
private LineRenderer lineRenderer;
[SerializeField]
private float speed;
[SerializeField]
private bool moveToFirstPositionOnStart;
public List<MoveOnCurvedLines> movingObjects = new List<MoveOnCurvedLines>();
void Start()
{
Vector3[] positions = GetPositions();
movingObjects = GameObject.FindGameObjectsWithTag("New Prefab").Select(go => go.GetComponent<MoveOnCurvedLines>().ToList();
foreach (MoveOnCurvedLines obj in movingObjects)
{
obj.Init(positions, speed, moveToFirstPositionOnStart);
}
StartCoroutine(TriggerObjects(false));
}
Vector3[] GetPositions()
{
Vector3[] positions = new Vector3[lineRenderer.positionCount];
//Get the positions which are shown in the inspector
lineRenderer.GetPositions(positions);
return positions;
}
IEnumerator TriggerObjects(bool delayFirstObject)
{
WaitForSeconds waitThreeSeconds = new WaitForSeconds(3);
if (delayFirstObject)
yield return waitThreeSeconds;
foreach (MoveOnCurvedLines obj in movingObjects)
{
obj.StartMoving();
yield return waitThreeSeconds;
}
}
}
Movement logic:
public class MoveOnCurvedLines : MonoBehaviour
{
private Transform myTransform;
private bool initialized;
private Vector3[] pos;
private int posIndex = 0;
private float speed;
private bool goForward = true;
private Coroutine moving;
public void Init(Vector3[] positions, float speed, bool instantlyMoveToFirstPosition)
{
myTransform = transform;
pos = positions;
this.speed = speed;
if (instantlyMoveToFirstPosition)
myTransform.position = positions[0];
initialized = true;
}
public void StartMoving()
{
if (initialized && moving == null)
moving = StartCoroutine(Move());
}
public void StopMoving()
{
if (moving != null)
{
StopCoroutine(moving);
moving = null;
}
}
private IEnumerator Move()
{
while (true)
{
Vector3 newPos = myTransform.position;
float distanceToTravel = speed * Time.deltaTime;
bool stillTraveling = true;
while (stillTraveling)
{
Vector3 oldPos = newPos;
newPos = Vector3.MoveTowards(oldPos, pos[posIndex], distanceToTravel);
distanceToTravel -= Vector3.Distance(newPos, oldPos);
if (newPos == pos[posIndex]) // Vector3 comparison is approximate so this is ok
{
// when you hit a waypoint:
if (goForward)
{
bool atLastOne = posIndex >= pos.Length - 1;
if (!atLastOne)
{
posIndex++;
}
else
{
posIndex--;
goForward = false;
}
}
else
{ // going backwards:
bool atFirstOne = posIndex <= 0;
if (!atFirstOne)
{
posIndex--;
}
else
{
posIndex++;
goForward = true;
}
}
}
else
{
stillTraveling = false;
}
}
myTransform.position = newPos;
}
}
}
MovementController only provides the necessary data, which all your objects share (e.g. the path), but every MoveOnCurvedLines object keeps track of it's progress independently.
Optimizations:
I cached transform in myTransform, since Unity's transform calls GetComponent<Transform>() every time producing unnecessary overhead.
Moving is done in a coroutine, not in Update, since checking n go bools every frame n objects do not move is unnecessary.
I changed your public fields to private ones getting serialized, since it's best practice to restrict access as much as possible. If you need to access them from another script in your project just make them public again.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Rotate : MonoBehaviour
{
public GameObject[] objectsToRotate;
public float duration = 5f;
public static bool desiredAngle = false;
private Vector3 lastFwd;
private bool startRot = true;
private void OnMouseDown()
{
if (startRot == true)
{
startRot = false;
StartCoroutine(StartRotationOfObjects());
}
}
private IEnumerator StartRotationOfObjects()
{
for (int i = 0; i < objectsToRotate.Length; i++)
{
// Random wait period before rotation starts
if (i == 0)
{
yield return new WaitForSeconds(0);
}
else
{
yield return new WaitForSeconds(Random.Range(0, 2f));
}
StartCoroutine(Rotates(objectsToRotate[i].transform, duration));
}
startRot = true;
}
private IEnumerator Rotates(Transform objectToRotate, float duration)
{
Quaternion startRot = objectToRotate.rotation;
float t = 0.0f;
lastFwd = objectToRotate.transform.forward;
while (t < duration)
{
t += Time.deltaTime;
objectToRotate.rotation = startRot * Quaternion.AngleAxis(t / duration * 360f, Vector3.up);
var curFwd = objectToRotate.transform.forward;
// measure the angle rotated since last frame:
var ang = Vector3.Angle(curFwd, lastFwd);
if (myApproximation(ang, 179f, 1f) == true)
{
desiredAngle = true;
}
yield return null;
}
objectToRotate.rotation = startRot;
desiredAngle = false;
}
private bool myApproximation(float a, float b, float tolerance)
{
return (Mathf.Abs(a - b) < tolerance);
}
}
I want to disable the OnMouseDown code so I will not be able to execute the Coroutine nonstop times. And after all the objects finished rotating then to enable the OnMouseDown again. I'm using the startRot flag for that but still it's true all the time and I can keep start the coroutine inside the OnMouseDown nonstop.
Below is a simplified version of your code.
private void OnMouseDown()
{
if (startRot == true)
{
startRot = false;
StartCoroutine(StartRotationOfObjects());
}
}
private IEnumerator StartRotationOfObjects()
{
for (int i = 0; i < objectsToRotate.Length; i++)
{
if (i == 0)
{
yield return new WaitForSeconds(0);
}
else
{
yield return new WaitForSeconds(Random.Range(0, 2f));
}
StartCoroutine(Rotates(objectsToRotate[i].transform, duration));
}
startRot = true;
}
private IEnumerator Rotates(Transform objectToRotate, float duration)
{
while (condition)
{
yield return null;
}
}
First, you have the OnMouseDown checking if true and it is on first run so it enters, sets it to false and start the coroutine.
Moving on with the loop. The first if check is useless since wait for 0 seconds. But it actually impacts the experience. So on first run, it enters the statement and basically does not wait. It then moves on to start the Rotates coroutine.
Entering the coroutine, it will reach a yield statement, place the coroutine in a list of coroutines to run and return. Back in the loop, it runs again, this time it will wait for to 2 seconds and run the next coroutine and so on until last one of the objectsToRotate collection.
Loop is done, startRot is set back to true. This means, even though, your coroutines may not be done, you can trigger new ones.
Your solution, either you want to wait for one rotation to be done before starting a new one with:
if (i == 0)
{
yield return new WaitForSeconds(0);
}
else
{
yield return new WaitForSeconds(Random.Range(0, 2f));
}
yield StartCoroutine(Rotates(objectsToRotate[i].transform, duration));
or you need to keep track of any coroutine running:
private int index = 0;
private IEnumerator StartRotationOfObjects()
{
for (int i = 0; i < objectsToRotate.Length; i++)
{
if (i == 0)
{
yield return new WaitForSeconds(0);
}
else
{
yield return new WaitForSeconds(Random.Range(0, 2f));
}
StartCoroutine(Rotates(objectsToRotate[i].transform, duration);
}
while(index > 0){ yield return null; }
startRot = true;
}
private IEnumerator Rotates(Transform objectToRotate, float duration)
{
index++;
while (condition)
{
yield return null;
}
index--;
}
Now anytime you start a coroutine, index is increased, at the end of the coroutine, it is decreased, in the main coroutine, you yield until index is back to 0.