I have a GameObject that adds and removes other valid GameObjects to a List as they drift through a collider trigger. Under certain conditions it will select an object in the List at random and replace it with another GameObject. It works fine for the most part but occasionally it will give an ArgumentOutOfRangeException error. Here is the code:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BubbleScriptNecroGrab : MonoBehaviour
{
[SerializeField] public List<GameObject> bubbleOnes = new List<GameObject>();
[SerializeField] public List<GameObject> necroAcolytes = new List<GameObject>();
[SerializeField] GameObject activeBubble;
[SerializeField] GameObject necroAcolyte;
bool necroReady = true;
void Update()
{
if (bubbleOnes.Count > 0 && necroReady == true)
{
StartCoroutine(bubbleSignal());
}
}
// These two functions add and remove eligible bubbles from its associated list
// as they pass through the collider.
private void OnTriggerEnter2D(Collider2D collision)
{
if (collision.gameObject.tag == "Bubble1")
{
GameObject other = collision.gameObject;
bubbleOnes.Add(other);
}
}
private void OnTriggerExit2D(Collider2D collision)
{
if(collision.gameObject.tag == "Bubble1")
{
GameObject other = collision.gameObject;
bubbleOnes.Remove(other);
}
}
// When this is called, select a random bubble from the list, delete it and
// replace it with a prefab.
IEnumerator bubbleSignal()
{
necroReady = false;
yield return new WaitForSeconds(Random.Range(0.1f, 1.5f));
int randomList = Random.Range(0, bubbleOnes.Count -1); // adding -1 reduced amount of errors
Vector3 targetBubblePos = bubbleOnes[randomList].transform.position; // Code seems to break on this line
Destroy(bubbleOnes[randomList]);
GameObject necroAcolyteClone = Instantiate(necroAcolyte, targetBubblePos, Quaternion.identity);
necroAcolyteClone.GetComponent<AcolyteScript>().activeTarget = transform.parent.gameObject;
necroReady = true;
}
}
What I suspect is happening is as the bubbleSignal function operates, it selects a large or largest value just as it gets removed from the list by drifting out of the collider. How do I fix this?
In general the -1 in
int randomList = Random.Range(0, bubbleOnes.Count -1);
does not do what you think.
The upper bound already is exclusive so this will never ever return the last index.
Beyond that I see nothing obviously wrong in your code (at this point) which would lead me to the assumption that bubbleOnes is empty and therefore randomList = 0 but since there is no element at all bubbleOnes[randomList] already throws the exception.
This could e.g. happen if OnTriggerExit2D is called while there is only a single element in the list before the bubbleSignal routine or more exactly the WaitForSeconds within it finished. In that case you would remove the last element from the list, leave an empty list and then WaitForSeconds would eventually finish and you reach your exception line with an empty list.
You should handle this case and do e.g.
IEnumerator bubbleSignal()
{
necroReady = false;
yield return new WaitForSeconds(Random.Range(0.1f, 1.5f));
if(bubbleOnes.Count > 0)
{
var randomList = Random.Range(0, bubbleOnes.Count);
var targetBubblePos = bubbleOnes[randomList].transform.position;
Destroy(bubbleOnes[randomList]);
var necroAcolyteClone = Instantiate(necroAcolyte, targetBubblePos, Quaternion.identity);
necroAcolyteClone.GetComponent<AcolyteScript>().activeTarget = transform.parent.gameObject;
}
necroReady = true;
}
In general in your use-case I would not use a Coroutine and poll-check in Update whether to run it or not. Here it would be way more flexible to directly stay in Update and do e.g.
[SerializeField] private float minTime = 0.1f;
[SerializeField] private float maxTime = 1.5f;
private float timer;
void Start()
{
timer = Random.Range(minTime, maxTime);
}
void Update()
{
if (bubbleOnes.Count > 0)
{
timer -= Time.deltaTime;
if(timer <= 0)
{
var randomList = Random.Range(0, bubbleOnes.Count);
var targetBubblePos = bubbleOnes[randomList].transform.position;
Destroy(bubbleOnes[randomList]);
var necroAcolyteClone = Instantiate(necroAcolyte, targetBubblePos, Quaternion.identity);
necroAcolyteClone.GetComponent<AcolyteScript>().activeTarget = transform.parent.gameObject;
timer += Random.Range(minTime, maxTime);
}
}
// [optionally]
// Instead of just continuing the counter from where it stopped last time
// you could also reset it while the list is empty
else
{
timer = Random.Range(minTime, maxTime);
}
}
This way you can be sure that there is no timing issue as all the code is only actually executed if the list is not empty in that moment.
Related
I am working on a flocking system in Unity and am new to c#. I am working with 2 scripts - 1 that manages the overall flock (FlockTest) and the other that manages particle behaviour (FlockParticleBehaviour). I have followed a tutorial which has public boolean values that control seeking behaviour in FlockParticleBehaviour through FlockTest. In play mode, I can toggle these booleans to change the goal seeking behaviour. However, I want to automate this toggling based on time (To add it to an AR session). I have added an if statement to void Update() in the FlockTest and when I hit play, the seekGoal and obedient boolean boxes switch on and off but nothing happens to the particles. I have tried using an invoke method which didn't work(no errors but boxes dont switch on and off) and thought about trying a coRoutine but I am not sure this will work since I don't want to stop and start my script. I am at a loss as to how to get the particles obeying the boolean in update. Am I meant to be referencing in my particle behaviour script's flock function? Very new so would love some help if anyone knows a better way forward!
FlockTest script (contains if statement)
using System.Collections.Generic;
using UnityEngine;
public class FlockTest : MonoBehaviour
{
public GameObject[] particles;
public GameObject particlePrefab;
public int particleCount = 10;
public Vector3 range = new Vector3(5,5,5);
public Vector3 innerLimit = new Vector3(1,1,1);
public bool seekGoal = true;
public bool obedient = true;
public bool willful = false;
[Range(0, 200)]
public int neighbourDistance =50;
[Range(0,2)]
public float maxForce = 0.5f;
[Range(0,5)]
public float maxvelocity = 2.0f;
// Start is called before the first frame update
void Start()
{
int time = (int)Time.time;
particles = new GameObject[particleCount];
for(int i = 0; i < particleCount; i++)
{
Vector3 particlePos = new Vector3(Random.Range(-range.x, range.x), Random.Range(-range.y, range.y), Random.Range(-range.z, range.z));
particles[i] = Instantiate(particlePrefab, this.transform.position + particlePos, Quaternion.identity) as GameObject;
particles[i].GetComponent<FlockParticleBehaviour>().manager = this.gameObject;
}
}
void Update()
// the toggles in the inspector are changing but nothing is happening with the particles.
{
int time = (int)Time.time;
if(time == 3f) {
seekGoal = false;
obedient = false;
willful = true;
}
if(time == 6f)
{
seekGoal = true;
obedient = true;
willful = false;
}
}
}
FlockParticleBehaviour script
using System.Collections.Generic;
using UnityEngine;
public class FlockParticleBehaviour : MonoBehaviour
{
public GameObject manager;
public Vector3 location = Vector3.zero;
public Vector3 velocity;
Vector3 goalPos = Vector3.zero;
Vector3 currentForce; //this is a current force position. pushes particle around by adding all the other forces
// Start is called before the first frame update
void Start()
{
velocity = new Vector3(Random.Range(0.01f, 0.1f), Random.Range(0.01f, 0.1f), Random.Range(0.01f, 0.1f));
location = new Vector3(this.gameObject.transform.position.x, this.gameObject.transform.position.y, this.gameObject.transform.position.z);
}
Vector3 seek(Vector3 target)
{
return(target - location);
}
void applyForce(Vector3 f)
{
Vector3 force = new Vector3(f.x, f.y, f.z);
if(force.magnitude > manager.GetComponent<FlockTest>().maxForce)
{
force = force.normalized;
force *= manager.GetComponent<FlockTest>().maxForce;
}
this.GetComponent<Rigidbody>().AddForce(force);
if(this.GetComponent<Rigidbody>().velocity.magnitude > manager.GetComponent<FlockTest>().maxvelocity)
{
this.GetComponent<Rigidbody>().velocity = this.GetComponent<Rigidbody>().velocity.normalized;
this.GetComponent<Rigidbody>().velocity *= manager.GetComponent<FlockTest>().maxvelocity;
}
Debug.DrawRay(this.transform.position, force, Color.white);
}
Vector3 align()
{
float neighbourdist = manager.GetComponent<FlockTest>().neighbourDistance;
Vector3 sum = Vector3.zero;
int count = 0;
foreach (GameObject other in manager.GetComponent<FlockTest>().particles)
{
if(other == this.gameObject) continue;
float d = Vector3.Distance(location, other.GetComponent<FlockParticleBehaviour>().location);
if (d < neighbourdist) {
sum += other.GetComponent<FlockParticleBehaviour>().velocity;
count++;
}
}
if (count >0)
{
sum /= count;
Vector3 steer = sum - velocity;
return steer;
}
return Vector3.zero;
}
Vector3 cohesion()
{
float neighbourdist = manager.GetComponent<FlockTest>().neighbourDistance;
Vector3 sum = Vector3.zero;
int count = 0;
foreach (GameObject other in manager.GetComponent<FlockTest>().particles)
{
if(other == this.gameObject) continue;
float d = Vector3.Distance(location, other.GetComponent<FlockParticleBehaviour>().location);
if(d < neighbourdist)
{
sum += other.GetComponent<FlockParticleBehaviour>().location;
count++;
}
}
if (count > 0)
{
sum /= count;
return seek(sum);
}
return Vector3.zero;
}
void flock()
{
location = this.transform.position;
velocity = this.GetComponent<Rigidbody>().velocity;
if(manager.GetComponent<FlockTest>().obedient && Random.Range(0,50) <=1)
{
Vector3 ali = align();
Vector3 coh = cohesion();
Vector3 gl;
if(manager.GetComponent<FlockTest>().seekGoal)
{
gl = seek(goalPos);
currentForce = gl + ali +coh;
}
else
currentForce = ali + coh;
currentForce = currentForce.normalized;
}
if(manager.GetComponent<FlockTest>().willful && Random.Range(0,50)<=1)
{
if(Random.Range(0,50)<1) //change direction
currentForce = new Vector3(Random.Range(0.01f, 0.1f), Random.Range(0.01f, 0.1f),Random.Range(0.01f, 0.1f));
}
applyForce(currentForce);
}
// Update is called once per frame
void Update()
{
flock();
goalPos = manager.transform.position;
}
}
Several points:
it is much easier and cleaner to set your flock manager directly as FlockTest, not GameObject to avoid GetComponent calls.
I cannot understand what you want to achieve by calling (int)Time.time and comparing it later with 3 and 6. Time.time returns the number of seconds that passed from the start of the application. So your code in Update method of FlockTest script will not have any chance to be called after the seventh second of your game passed. So obedient will always be true and willful will always be false after the seventh second.
Your Random.Range(0, 50) <= 1 is quite a low chance. It will return an int value from 0 to 49, so it is only a 2% chance that your changes in FlockTest will apply to FlockParticleBehaviour instance. Is it what you wanted to get? You can try to remove this random from the if statement to make this chance 100% and check if this is an issue.
Right now it seems like the chance of changing something is too low to see it in several seconds of the game. As I've said above, after the seventh second your bool values will never change.
I used an item script that I found online that picks up an Item. It was intended for first-person raycasting but I changed it to detect if my player triggers the item. I wanted the item to lock to my player's hand 1 second after the animation plays. I tried invoking but I learned I can't do that with parameters. I then tried Coroutines but they were complicated and I could not get it to work. I made a new void that I invoke after 1 second which I want to start my PickItem void. However, I don't know how to do this. I don't understand how parameters work either.
public int number = 1;
Animator animator;
private int i;
private GameObject[] Item;
private bool inrange;
// Reference to the character camera.
[SerializeField]
private Camera characterCamera;
// Reference to the slot for holding picked item.
[SerializeField]
private Transform slot;
// Reference to the currently held item.
private PickableItem pickedItem;
My OnTriggerStay code.
private void OnTriggerStay(Collider other)
{
if (other.gameObject.tag == "Item")
{
inrange = true;
if (inrange == true && (number % 2) == 1)
{
// Check if object is pickable
var pickable = other.gameObject.GetComponent<PickableItem>();
// If object has PickableItem class
if (pickable)
{
//Invoke delay after 1 seconds
Invoke("delay", 1f);
}
}
}
}
My delay void:
public void delay()
{
number = number + 1;
PickItem();
}
My PickItem void:
// I don't under stand this line of code either
/// <param name="item">Item.</param>
public void PickItem(PickableItem item)
{
// Assign reference
pickedItem = item;
// Disable rigidbody and reset velocities
item.Rb.isKinematic = true;
item.Rb.velocity = Vector3.zero;
item.Rb.angularVelocity = Vector3.zero;
// Set Slot as a parent
item.transform.SetParent(slot);
// Reset position and rotation
item.transform.localPosition = Vector3.zero;
item.transform.localEulerAngles = Vector3.zero;
item.GetComponent<MeshCollider>().enabled = false;
}
I know coroutines are probably better in this case but I could not get them working.
Coroutines make this simple. Use WaitForSeconds to create the delay:
IEnumerator DelayPickup(PickableItem item)
{
yield return new WaitForSeconds(1f);
number++;
PickItem(item);
}
Call it like this:
private void OnTriggerStay(Collider other)
{
if (other.gameObject.CompareTag("Item"))
{
inrange = true;
// inrange is true here by assignment, don't need to check again
if (number % 2 == 1)
{
// Check if object is pickable
var pickable = other.gameObject.GetComponent<PickableItem>();
// If object has PickableItem class
if (pickable)
{
StartCoroutine(DelayPickup(pickable));
}
}
}
}
By the way, they aren't called "voids", they're methods with a return type of void.
Alright, so here's what's happening: When I hit play and left click to shoot, unity editor freezes and I have to do the old Ctrl + Alt + Del, now, I am almost certain this script is the source of the issue, because when a bullet is shot, this script is immediately added to it, so here's the script(It's called BulletLife.cs, just letting you know)
using System.Timers;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using UnityEngine;
public class BulletLife : MonoBehaviour
{
public GameObject bullet;
public double bulletLifeSpan = 3;
bool bulletLifeEnded;
public LayerMask targetMask;
bool hasHitTarget;
// Start is called before the first frame update
void Start()
{
var bulletAge = new System.Timers.Timer(bulletLifeSpan * 1000);
bulletAge.Elapsed += OnTimedEvent;
bulletAge.AutoReset = false;
while(hasHitTarget == false && bulletLifeEnded == false) {
hasHitTarget = Physics.CheckSphere(bullet.transform.position, bullet.transform.localScale.y, targetMask);
}
Destroy(bullet);
Debug.Log("Finish");
}
private void OnTimedEvent(System.Object Source, ElapsedEventArgs e) {
bulletLifeEnded = true;
}
}
Also, here's the Shoot.cs script:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Shoot : MonoBehaviour
{
public Transform gun;
public GameObject bullet;
public LayerMask targetMask;
public float bulletSpeed = 1000f;
bool hasHitTarget = false;
// Update is called once per frame
void Update()
{
if(Input.GetButtonDown("LeftClick")) {
GameObject bulletInstance;
bulletInstance = Instantiate(bullet, gun.position, new Quaternion(gun.rotation.w, gun.rotation.x, gun.forward.y, gun.rotation.z));
bulletInstance.AddComponent<Rigidbody>();
bulletInstance.GetComponent<Rigidbody>().useGravity = false;
bulletInstance.GetComponent<Rigidbody>().AddForce(gun.up * bulletSpeed);
bulletInstance.AddComponent<BulletLife>();
bulletInstance.GetComponent<BulletLife>().bullet = bulletInstance;
}
}
}
NOTE: I am using Unity 2019.4.15f1
Well everytime you instantiate a bullet in Start you do
while(hasHitTarget == false && bulletLifeEnded == false)
{
hasHitTarget = Physics.CheckSphere(bullet.transform.position, bullet.transform.localScale.y, targetMask);
}
this loop will never finish since none of the conditions is changed inside the loop. There either is a hit or not .. but then the parameters for the raycast are never changed, the position isn't updated since you are still in the same frame => endless loop => freeze the main thread completely.
What you rather wanted to do is move that thing to Update which is called once a frame like e.g.
//public GameObject bullet; // not needed
public double bulletLifeSpan = 3;
//bool bulletLifeEnded; // not needed
public LayerMask targetMask;
//bool hasHitTarget; // not needed
void Start()
{
var bulletAge = new System.Timers.Timer(bulletLifeSpan * 1000);
bulletAge.Elapsed += OnTimedEvent;
bulletAge.AutoReset = false;
}
private void Update()
{
if(Physics.CheckSphere(transform.position, transform.localScale.y, targetMask))
{
Destroy(gameObject);
Debug.Log("Finish");
}
}
private void OnTimedEvent(System.Object Source, ElapsedEventArgs e)
{
Destroy(gameObject);
Debug.Log("Finish");
}
Or make it a single Coroutine
// If Start returns IEnumerator it is automatically started as Coroutine
// So no need to start an extra routine
private IEnumerator Start()
{
// Keeps track of how long your bullet exists already
var bulletAge = 0f;
while(bulletAge < bulletLifeSpan && !Physics.CheckSphere(transform.position, transform.localScale.y, targetMask))
{
// Increase by the time passed since last frame
bulletAge += Time.deltaTime;
// "Pause" this routine, render this frame
// and continue from here in the next frame
yield return null;
}
Destroy(gameObject);
Debug.Log("Finish");
}
Btw note that in Shoot you can shorten this a lot
void Update()
{
if(Input.GetButtonDown("LeftClick"))
{
// Note that your quaternion made no sense -> simply pass in the gun.rotation
var bulletInstance = Instantiate(bullet, gun.position, gun.rotation);
var rb = bulletInstance.AddComponent<Rigidbody>();
rb.useGravity = false;
rb.AddForce(gun.up * bulletSpeed);
var life = bulletInstance.AddComponent<BulletLife>();
// Assigning the gameObject reference is completely unnecessary
// within BulletLife simply use "gameObject" as show before
}
}
You could shorten this even more by making sure these components already exist on your prefab object and are configured correctly. Then you wouldn't need any of these line but just Instantiate it.
And finally you shouldn't use thisCheckSphere at all but rather let Unity handle its Collision detection itself and use OnCollisionEnter and configure your Collision Layers according to your needs!
The issue with your solution is: If your bullet moves fast it might simply pass a target without your CheckSphere noting it namely if its velocity is higher then localScale.y * 2.
Your Start method is blocking, thus freezing your game.
You'll have to use Update or a Coroutine to make your hit tests.
public class BulletLife : MonoBehaviour
{
public GameObject bullet;
public double bulletLifeSpan = 3;
bool bulletLifeEnded;
public LayerMask targetMask;
bool hasHitTarget;
// Start is called before the first frame update
void Start()
{
StartCoroutine(CheckHit(0, bulletLifeSpan));
}
private IEnumerator CheckHit(float interval, float lifetime){
bool checkEveryFrame = interval <= 0;
WaitForSeconds wait = checkEveryFrame ? null : new WaitForSeconds(interval);
while(lifetime > 0){
yield return wait;
lifetime = lifetime - (checkEveryFrame ? Time.deltaTime : interval);
hasHitTarget = Physics.CheckSphere(bullet.transform.position, bullet.transform.localScale.y, targetMask);
}
bulletLifeEnded = true;
Destroy(bullet);
Debug.Log("Finish");
}
}
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.
I'm having difficulty getting my gold pickups to respawn after they've been destroyed on death. The idea is, if the player fails to pick up the 5 gold bars, activates a checkpoint, and dies, the current gold is destroyed and it resets once the screen has faded from black.
I currently have a Coroutine in my Health Manager that runs correctly if the player dies and resets them. I have a Gold Pickup script that destroys the gold if they haven't been picked up. I just can't seem to get them to re-instantiate. I've tried adding the instantiate code within the Health Manager's coroutine and within the Gold Pickup script. Nothing seems to work. If I'm not getting errors saying 'Array index is out of range' it's 'object reference not set to an instance of an object' etc.
public class GoldPickup : MonoBehaviour{
public int value;
public GameObject pickupEffect;
public GameObject[] goldBarArray;
public HealthManager healthManager;
public Checkpoint checkpoint;
private Vector3 goldRespawnPoint;
private Quaternion goldStartPosition;
void Start()
{
//To destroy multiple objects at once, use FindGameObjectsWithTag.
//GetComponent is considered more efficient than FindObjectOfType, but the latter avoids any errors saying an object reference hasn't been set.
goldBarArray = GameObject.FindGameObjectsWithTag("Gold");
healthManager = FindObjectOfType<HealthManager>();
//FindObjectOfType<Checkpoint>();
checkpoint = FindObjectOfType<Checkpoint>();
goldRespawnPoint = transform.position;
goldStartPosition = transform.rotation;
}
public void OnTriggerEnter(Collider other)
{
if (other.gameObject.CompareTag("Player"))
{
FindObjectOfType<GameManager>().AddGold(value);
Instantiate(pickupEffect, transform.position, transform.rotation);
Destroy(gameObject);
}
}
public void DestroyGold()
{
//For Statics, an object reference isn't necessary. Use the FindObjectOfType to find the appropriate script and reference the Type, such as HealthManager.
if (checkpoint.checkpoint1On == false)
{
foreach (GameObject Gold in goldBarArray)
{
Destroy(Gold);
Instantiate(goldBarArray[5], goldRespawnPoint, goldStartPosition);
goldRespawnPoint = transform.position;
goldStartPosition = transform.rotation;
//healthManager.RespawnCo();
}
}
}
/*public void GoldReset()
{
if (healthManager.isRespawning == true)
{
if (checkpoint.checkpoint1On == false)
{
StartCoroutine("GoldRespawnCo");
}
}
else if (_respawnCoroutine != null)
{
StopCoroutine(_respawnCoroutine);
_respawnCoroutine = StartCoroutine("GoldRespawnCo");
}*/
/*public IEnumerator GoldRespawnCo()
{
if (checkpoint.checkpoint1On == false)
{
Instantiate(goldPrefab, goldRespawnPoint, goldStartPosition);
transform.position = goldRespawnPoint;
transform.rotation = goldStartPosition;
}
else
{
yield return null;
}
}*/
/*if (thePlayer.gameObject.activeInHierarchy == false)
{
Destroy(gameObject);
Instantiate(goldBar, transform.position, transform.rotation);
}
else
{
if (thePlayer.gameObject.activeInHierarchy == true)
{
transform.position = respawnPoint;
transform.rotation = startPosition;
}
}*/
}
public class HealthManager : MonoBehaviour
//The counters will count down and will keep counting down based on the length variables
public int maxHealth;
public int currentHealth;
public PlayerController thePlayer;
//public GoldPickup goldPickup;
//public GoldPickup[] goldPickup;
public float invincibilityLength;
public Renderer playerRenderer;
public float flashLength;
public float respawnLength;
public GameObject deathEffect;
public Image blackScreen;
public float fadeSpeed;
public float waitForFade;
public bool isRespawning;
//public GameObject goldBar;
//To reference another script's function, such as in the DeathTrigger script, make a public DeathTrigger, give it a reference name, and put it into the Start function. Use the reference name and assign it using GetComponent. Call another script's method by using the reference name, followed by a dot and the name of the method. Eg: deathTrigger.DestroyGold().
private Quaternion startPosition;
//private Quaternion goldPosition;
private float flashCounter;
private float invincibilityCounter;
private Vector3 respawnPoint;
//private Vector3 goldRespawnPoint;
private bool isFadetoBlack;
private bool isFadefromBlack;
//private Coroutine _respawnCoroutine;
//private Vector3 goldRespawnPoint;
//private Quaternion goldStartPosition;
void Start()
{
currentHealth = maxHealth;
respawnPoint = thePlayer.transform.position;
startPosition = thePlayer.transform.rotation;
//goldPickup = GetComponent<GoldPickup>();
//goldRespawnPoint = goldBar.transform.position;
//goldStartPosition = goldBar.transform.rotation;
//goldRespawnPoint = transform.position;
//goldStartPosition = transform.rotation;
//goldPickup = FindObjectOfType<GoldPickup>();
//goldRespawnPoint = goldBar.transform.position;
//goldPosition = goldBar.transform.rotation;
}
void Update()
{
//These functions are checked every frame until the player takes damage
if (invincibilityCounter > 0)
{
invincibilityCounter -= Time.deltaTime;
flashCounter -= Time.deltaTime;
if (flashCounter <= 0)
//The Flash Counter is currently set at 0.1 and will be within the 0 region as it counts down. During this period, the playerRenderer will alternate between on and off
{
playerRenderer.enabled = !playerRenderer.enabled;
//The Flash Counter will keep counting down and reloop depending on the Flash Length time
flashCounter = flashLength;
}
//This makes sure after the flashing and invincibility has worn off that the player renderer is always turned back on so you can see the player
if (invincibilityCounter <= 0)
{
playerRenderer.enabled = true;
}
}
if (isFadetoBlack)
{
blackScreen.color = new Color(blackScreen.color.r, blackScreen.color.g, blackScreen.color.b, Mathf.MoveTowards(blackScreen.color.a, 1f, fadeSpeed * Time.deltaTime));
if (blackScreen.color.a == 1f)
{
isFadetoBlack = false;
}
}
if (isFadefromBlack)
{
blackScreen.color = new Color(blackScreen.color.r, blackScreen.color.g, blackScreen.color.b, Mathf.MoveTowards(blackScreen.color.a, 0f, fadeSpeed * Time.deltaTime));
if (blackScreen.color.a == 0f)
{
isFadefromBlack = false;
}
}
}
public void HurtPlayer(int damage, Vector3 direction)
{
//If the invincibility countdown reaches zero it stops, making you no longer invincible and prone to taking damage again
if (invincibilityCounter <= 0)
{
currentHealth -= damage;
if (currentHealth <= 0)
{
Respawn();
}
else
{
thePlayer.Knockback(direction);
invincibilityCounter = invincibilityLength;
playerRenderer.enabled = false;
flashCounter = flashLength;
}
}
}
public void Respawn()
{
//A StartCoroutine must be set up before the IEnumerator can begin
if (!isRespawning)
{
StartCoroutine("RespawnCo");
}
}
//IEnumerators or Coroutines will execute the code separately at specified times while the rest of the code in a codeblock will carry on executing as normal.
//To prevent an error appearing below the name of the Coroutine, be sure to place a yield return somewhere within the code block. Either yield return null or a new WaitForSeconds.
public IEnumerator RespawnCo()
{
if (GameManager.currentGold < 5)
{
isRespawning = true;
thePlayer.gameObject.SetActive(false);
Instantiate(deathEffect, respawnPoint, startPosition);
yield return new WaitForSeconds(respawnLength);
isFadetoBlack = true;
yield return new WaitForSeconds(waitForFade);
//To reference another script's function quickly and just the once, use the FindObjectOfType function. This is considered to be slow however.
FindObjectOfType<GoldPickup>().DestroyGold();
//GetComponent<GoldPickup>().DestroyGold();
//Instantiate(goldBar, goldRespawnPoint, Quaternion.identity);
isFadefromBlack = true;
//goldRespawnPoint = goldBar.transform.position;
//goldStartPosition = goldBar.transform.rotation;
isRespawning = false;
thePlayer.gameObject.SetActive(true);
thePlayer.transform.position = respawnPoint;
thePlayer.transform.rotation = startPosition;
currentHealth = maxHealth;
invincibilityCounter = invincibilityLength;
playerRenderer.enabled = false;
flashCounter = flashLength;
GameManager.currentGold = 0;
GetComponent<GameManager>().SetCountText();
StopCoroutine("RespawnCo");
/*isRespawning = true;
thePlayer.gameObject.SetActive(false);
yield return new WaitForSeconds(respawnLength);
isFadetoBlack = true;
yield return new WaitForSeconds(waitForFade);
isFadefromBlack = true;
invincibilityCounter = invincibilityLength;
playerRenderer.enabled = false;
flashCounter = flashLength;
SceneManager.LoadScene("Level 1");
GameManager.currentGold = 0;*/
}
else if(GameManager.currentGold >= 5)
{
isRespawning = true;
thePlayer.gameObject.SetActive(false);
Instantiate(deathEffect, respawnPoint, startPosition);
yield return new WaitForSeconds(respawnLength);
isFadetoBlack = true;
yield return new WaitForSeconds(waitForFade);
isFadefromBlack = true;
isRespawning = false;
thePlayer.gameObject.SetActive(true);
thePlayer.transform.position = respawnPoint;
thePlayer.transform.rotation = startPosition;
currentHealth = maxHealth;
invincibilityCounter = invincibilityLength;
playerRenderer.enabled = false;
flashCounter = flashLength;
}
}
/*public void HealPlayer(int healAmount)
{
currentHealth += healAmount;
if(currentHealth > maxHealth)
{
currentHealth = maxHealth;
}
}*/
public void SetSpawnPoint(Vector3 newPosition)
{
respawnPoint = newPosition;
}
public class Checkpoint : MonoBehaviour
public HealthManager theHealthManager;
public Renderer cpRenderer;
public Renderer postRenderer;
public SpriteRenderer pcRenderer;
public Material cpOff;
public Material cpOn;
public Material postOff;
public Material postOn;
public GameObject[] infoPanels;
public bool checkpoint1On;
//Make sure to assign a value to a bool with '=' and in an 'if' statement somewhere in the code to prevent warnings.
//private bool checkpoint1IsActivated;
private bool infoPanel1Activated;
void Start()
{
theHealthManager = FindObjectOfType<HealthManager>();
}
void Update()
//Key presses are better handled in the Update function and will recognise keys being pressed once every frame.
{
if (checkpoint1On == true)
{
if (infoPanel1Activated == false)
{
if (Input.GetKeyDown(KeyCode.Space))
{
infoPanels[0].SetActive(true);
infoPanel1Activated = true;
}
}
else
{
if (infoPanel1Activated == true)
{
if (Input.GetKeyDown(KeyCode.Space))
{
infoPanels[0].SetActive(false);
infoPanel1Activated = false;
}
}
}
}
}
public void Checkpoint1On()
{
cpRenderer.material = cpOn;
postRenderer.material = postOn;
pcRenderer.color = new Color(1f, 1f, 1f, 1f);
checkpoint1On = true;
}
//[] makes a variable an Array (a list). The 'foreach' loop will check through all the Checkpoint objects
//Checkpoint[] checkpoints = FindObjectsOfType<Checkpoint>();
//For each Checkpoint Array called 'checkpoints', look for 'cp' and turn the others in the list off
/*foreach (Checkpoint cp in checkpoints)
{
cp.CheckpointOff();
}
theRenderer.material = cpOn;*/
public void Checkpoint1Off()
{
cpRenderer.material = cpOff;
postRenderer.material = postOff;
pcRenderer.color = new Color(1f, 1f, 1f, 5f);
checkpoint1On = false;
}
public void OnTriggerStay(Collider other)
{
if (other.gameObject.CompareTag("Player"))
{
if (GameManager.currentGold >= 5)
{
if (Input.GetKeyDown(KeyCode.Return))
{
theHealthManager.SetSpawnPoint(transform.position);
Checkpoint1On();
checkpoint1On = true;
}
}
else if (GameManager.currentGold <= 5)
{
checkpoint1On = false;
}
}
}
In your DestroyGold() function, you instantiate the gold like this:
foreach (GameObject Gold in goldBarArray)
{
Destroy(Gold);
Instantiate(goldBarArray[5], goldRespawnPoint, goldStartPosition);
goldRespawnPoint = transform.position;
goldStartPosition = transform.rotation;
//healthManager.RespawnCo();
}
But transform.position and transform.rotation only get the position and rotation of the current object (i.e. whatever your script is attached to). So not only are you spawning all the gold in the same spot, it's spawning the gold at the location of the object that holds your script, not where you actually want it to go!
Without knowing much about the objects in your scene, here's what I can tell you: try creating a Transform[] to store the locations where you want to respawn the gold. Also, make sure you assign the goldRespawnPoint and goldStartPosition BEFORE you call Instantiate() in your foreach loop. Finally, just a general tip: you should never use variable == true or variable == false in an if statement. You can just use if(variable) or if(!variable), respectively. It will work just the same while being more readable and reducing the amount of code you need to write.
EDIT 1: In response to comments, I've added specific code examples for implementing these suggestions.
To start, you're probably getting the out of range error because of goldBarArray[5]. Since arrays start at index 0, you can only access up to element n-1 in a size n array. More on how to fix this in the next step.
Now for the Transform array. In the area where you declare your public variables (at the top of the script), add the line
public Transform[] spawnPoints;
Then, back in Unity you will be able to assign those spawn points in the Inspector.
EDIT 2: Additionally, in the foreach loop you're trying to instantiate one of the gold bars from the scene, but those are getting deleted with the Destroy(Gold); statement. Instead, you should be instantiating from the prefab which won't get destroyed. To do this, add
public GameObject goldPrefab;
up with the rest of your public variables. Then, in the Editor create a prefab by dragging one of the gold bars from the Hierarchy into your Assets folder. Finally, set that prefab to be the value of goldPrefab in the Inspector.
Now, you actually can clean up your foreach loop a little bit. You can get rid of the goldRespawnPoint and goldStartPosition lines because the respawn locations will be contained in the Transform array we just created. Again, without knowing how your scene is structured I've needed to just make an educated guess about what will work. Give this loop a try:
int spawnPointCounter = 0;
foreach(GameObject Gold in goldBarArray){
Destroy(Gold);
Transform currentSP = spawnPoints[spawnPointCounter];
Instantiate(goldPrefab, currentSP.position, currentSP.rotation);
spawnPointCounter++;
}