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.
Related
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.
i have a gameobject that goes up if it stays in collision for 5 Secs, the problem is it only work once , i tried calling the OnGUI in the update when ever the ToggleGUI = true but did't work
public float elapsedTime = 0f;
bool ToggleGUI = false;
bool isCreated = false;
Vector3 firstpos;
private void Update()
{
if(ToggleGUI == true)
{
OnGUI();
}
}
void OnTriggerStay(Collider other)
{
elapsedTime += Time.deltaTime;
if (elapsedTime >= 5.0f)
{
ToggleGUI = true;
}
}
void OnTriggerExit(Collider other)
{
elapsedTime = 0f;
}
void OnGUI()
{
if (ToggleGUI == true)
{
if (!isCreated)
{
firstpos = transform.position;
firstpos.y += 2f;
transform.position = firstpos;
isCreated = true;
}
}
}
after isCreated is set to true there is no place in your code where that variable is set again to false, therefore it can get inside the condition that moves the gameobject just once for the lifecycle of the script.
FYI when you pass a bool to an if you don't need to write ==true to have it execute when that variable is true, the name of the variable is enough
Your code seems a bit to complex for what you are trying to achieve.
If following your description what you want is just that the object goes up by 2f units every time you have stayed within a collider for 5 seconds I would simply use a Corouinte like e.g.
// The currently running Coroutine
private Coroutine _routine;
private int currentTriggers;
private void OnTriggerEnter(Collider other)
{
currentTriggers++;
// Start a routine if none is running already
if(_routine == null)
{
_routine = StartCorouine (Routine());
}
}
private void OnTriggerExit(Collider other)
{
currentTriggers--;
// If there are still other triggers do nothing
if(currentTriggers > 0) return;
// Cancel the routine if one is running
if(_routine != null)
{
StopCoroutine(_routine);
_routine = null;
}
}
IEnumerator Routine()
{
// As the name says wait 5 seconds
yield return new WaitForSeconds (5f);
// This is only reached if the routine wasn't stopped before the time passed
// Then move your object up
transform.position += Vector3.up * 2f;
_routine = null;
}
In general be very careful with your method names!
OnGUI is a special built-in event message that is called for handling events such as Keyboard or mouse input events, Repaint calls from the UI, etc. It might get called multiple times within a single frame and is absolutely not what you want to do here and for sure you should not call this method "manually" from Update.
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");
}
}
I have a problem with the correct detection of object proximities using distance calculations and hope one of you can help me.
What i want:
There are several instantiated game objects with the same tag in my scene and I want to change their material color if their distance on the x and z axis is below "1". For this I iterate over a list of all objects and compare their position with the position of the current game object.
The problem:
Material colors are changing randomly on colliding objects and sometimes don't change back once the collision is over.
My code so far:
public class DistanceTester : MonoBehaviour
{
void Start()
{
}
void Update()
{
var menschen = GameObject.FindGameObjectsWithTag("Mensch");
float positionX = transform.position.x;
float positionZ = transform.position.z;
foreach (GameObject mensch in menschen)
{
float distanceX = Mathf.Abs(mensch.transform.position.x - positionX);
float distanceZ = Mathf.Abs(mensch.transform.position.z - positionZ);
if (gameObject != mensch) //Making sure the object is not the same
{
if (distanceX <= 1 && distanceZ <= 1)
{
GetComponent<Renderer>().material.color = Color.red;
mensch.GetComponent<Renderer>().material.color = Color.red;
}
else
{
GetComponent<Renderer>().material.color = Color.green;
mensch.GetComponent<Renderer>().material.color = Color.green;
}
}
}
}
}
I already tried to use triggers for collision detection but would like to use a more efficient way like in my example above.
The main issue is probbaly that you also set
GetComponent<Renderer>().material.color = ...;
so what if you are close to menschen[0] but far away from menschen[1] ?
→ You reset your color always with the result of the last item in menschen!
It sounds like you should rather only handle your own object since all of the other objects do the same thing right?
using Sytsem.Linq;
public class DistanceTester : MonoBehaviour
{
// reference this via the Inspector already
[SerializeField] private Renderer _renderer;
private void Awake()
{
// As fallback get it ONCE
if(!_renderer) _renderer = GetComponent<Renderer>();
}
private void Update()
{
// If possible you should also store this ONCE
var menschen = GameObject.FindGameObjectsWithTag("Mensch");
// This checks if ANY of the objects that is not this object is clsoe enough
if(menschen.Where(m => m != gameObject).Any(m => (transform.position - m.transform.position).sqrMagnitude < 1))
{
_renderer.material.color = Color.red;
}
else
{
_renderer.material.color = Color.green;
}
}
}
Where this Linq expression using Where and Any
menschen.Where(m => m!= gameObject).Any(m => (transform.position - m.transform.position).sqrMagnitude < 1)
Basically equals doing something like
var isClose = false;
foreach(var m in menschen)
{
if(m == gameObject) continue;
if((transform.position - m.transform.position).sqrMagnitude < 1)
{
isClose = true;
break;
}
}
if(isClose)
{
...
Note that it would still be more efficient if you can store the result of FindGameObjectsWithTag once instead of obtaining it every frame.
Assuming that any of your Mensch objects will have the component DistanceTester you could even implement some sort of "auto-detection" by using a pattern like
public class DistanceTester : MonoBehaviour
{
private static HashSet<DistanceTester> _instances = new HashSet<DistanceTester>();
private void Awake()
{
_instances.Add(this);
...
}
private void OnDestroy()
{
_instances.Remove(this);
}
...
}
Then you could quite efficient iterate through the _instances instead.
Even more efficient though would actually be to iterate only once from a global controller instead of doing that in each and every instance of DistanceTester!
I am spawning objects on startup,(maxObj = 75) then destroying Obj's on event and disabling spawner Obj. When player wants they can re enable spawner. I need count to start at 0 on enable. Another 75 obj's spawn and are then destroyed. etc. Appreciate any help thanks.
enter code here
private static readonly float _spawnTime = 0.125f;
[SerializeField]
private GameObject _asteroidObject = null;
[SerializeField]
private int _maxObjects = 0;
private int _spawnedObjects = 0;
private float _time = 0;
private void Update()
{
if(_spawnedObjects < _maxObjects)
{
if(_time > _spawnTime)
{
Instantiate(_asteroidObject, transform.position, Quaternion.identity);
++_spawnedObjects;
_time = 0;
}
_time += Time.smoothDeltaTime;
}
}
Touch it is quite unclear how a User shall be able to start the spawn again I would recommend to rather use a Coroutine in general. They are like little temporary Update blocks but easier to control and maintain. It is also more efficient since it doesn't call the Update method every frame also when the maxObjects amount is already reached and nothing is going to happen anyway
[SerializeField]
private GameObject _asteroidPrefab;
[SerializeField]
private float _spawnInterval = 0.125f;
[SerializeField]
private int _maxObjects;
private bool _canStart = true;
// However your player calls this method
public void StartSpawn()
{
// Only start spawning if there is no other spawn routine already running
if(_canStart) StartCoroutine(Spawn());
}
private IEnumerator Spawn()
{
// Just in case ignore if another routine is already running
if(!_canStart) yield break;
// block concurrent routines
_canStart = false;
var interval = new WaitForSeconds(_spawnInterval);
for(var i = 0; i < _maxObjects; i++)
{
Instantiate(_asteroidObject, transform.position, Quaternion.identity);
// yield makes Unity pause this routine and render the frame
// and continue from here in the next frame
// Then since we yield return another IEnumerator in thi case WaitForSconds
// it results exactly in this: Holds here for given seconds and then goes to the next iteration
yield return interval;
}
// re-enable the StartSpawn
_canStart = true;
}
Then in case you additionally also want to automatically start spawning in the beginning you can simply call it in
private void Start()
{
StartSpawn();
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SpawnObj : MonoBehaviour
{
[SerializeField]
private GameObject _asteroidPrefab;
[SerializeField]
private float _spawnInterval = 0.125f;
[SerializeField]
private int _maxObjects;
private bool _canStart = true;
private void Start()
{
StartSpawn();
}
// However your player calls this method
public void StartSpawn()
{
// Only start spawning if there is no other spawn routine already running
if (_canStart) StartCoroutine(Spawn());
}
private IEnumerator Spawn()
{
// Just in case ignore if another routine is already running
if (!_canStart) yield break;
// block concurrent routines
_canStart = false;
var interval = new WaitForSeconds(_spawnInterval);
for (var i = 0; i < _maxObjects; i++)
{
Instantiate(_asteroidPrefab, transform.position, Quaternion.identity);
// yield makes Unity pause this routine and render the frame
// and continue from here in the next frame
// Then since we yield return another IEnumerator in thi case WaitForSconds
// it results exactly in this: Holds here for given seconds and then goes to the next iteration
yield return interval;
}
// re-enable the StartSpawn
_canStart = true;
}
}