I am trying to learn unity. This is something I would use in Javascript so I was hoping to find a way to do this in C#, where you take the collision variable and use the reference to have another function clean up the variables after you are finished.
Edit
I have been practicing since posting this, I am trying to pass a reference to another class to handle the turn on and off of the variable.
private void OnTriggerEnter(Collider collision) {
if(collision.gameObject.CompareTag("Enemy")) {
PowerUpRoutine(ref collision.gameObject.GetComponent<enemy>().powerup);
Destroy(gameObject);
}
if (collision.gameObject.CompareTag("Player")) {
PowerUpRoutine(ref collision.gameObject.GetComponent<player>().powerup);
Destroy(gameObject);
}
}
IEnumerator PowerUpRoutine(float target) {
target = 1;
yield return new WaitForSeconds(5);
target = 0;
}
I have also tried not passing powerup, but just the object and it still errors. Is there anyway to accomplish this?
Original
private void OnTriggerEnter(Collider collision) {
if(collision.gameObject.CompareTag("Enemy")) {
collision.gameObject.GetComponent<enemy>().powerup = 1;
StartCoroutine(removePower(collision.gameObject.GetComponent<enemy>()));
Destroy(gameObject);
}
if (collision.gameObject.CompareTag("Player")) {
collision.gameObject.GetComponent<player>().powerup = 1;
StartCoroutine(removePower(collision.gameObject.GetComponent<player>()));
Destroy(gameObject);
}
}
IEnumerator removePower(GameObject target) {
yield return new WaitForSeconds(5);
target.powerup = 0;
}
First if all there is a general flaw in your approach: When you
Destroy(gameObject);
this object this component is attached to then also all Coroutines are immediately canceled.
I would therefore start the coroutine rather on the target itself (see example below).
And then your classes should have a common interface or base class like for example
Solution 1 - Common Base Class
public abstract class Character : MonoBehaviour
{
public float powerup;
// And other common members
}
And then you inherit
public class Player : Character
{
// Additional player specific stuff
}
and
public class Enemy : Character
{
// Additional enemy specific stuff
}
Solution 2 - Common Interface
if you rather want to go for an interface
public interface ICharacter
{
float powerup { get; set; }
}
And then both your classes have to implement that
public class Player : MonoBehaviour, ICharacter
{
public float powerup { get; set; }
// Additional player specific stuff
}
and
public class Enemy : MonoBehaviour, ICharacter
{
public float powerup { get; set; }
// Additional player specific stuff
}
And then your collision code could simply be
private void OnTriggerEnter(Collider collision)
{
// TryGetComponent now finds anything inherited from Character
// you don't even need to check the tags
if(collision.TryGetComponent<Character>(out var character))
// Or if using the interface
//if(collision.TryGetComponent<ICharacter>(out var character)
{
// Let the character run this coroutine without even having to know what it does
character.StartCoroutine(PowerUpRoutine(character));
// This is now ok and not terminating the Coroutine since the routine is run by character itself
Destroy(gameObject);
}
}
IEnumerator PowerUpRoutine(Character target)
// or if using the interface accordingly
//IEnumerator PowerUpRoutine(ICharacter target)
{
// Have in mind though that this routine is now running on a different object
// at a time where this component is already destroyed => You can't reference to anything of this component in here!
target.powerup = 1;
yield return new WaitForSeconds(5);
target.powerup = 0;
}
Solution 3 - Simple Overload
Or as last resort if the before two is not an option for whatever reason you could of course also simply have two overloads
IEnumerator PowerUpRoutine(Player target)
{
target.powerup = 1;
yield return new WaitForSeconds(5);
target.powerup = 0;
}
IEnumerator PowerUpRoutine(Enemy target)
{
target.powerup = 1;
yield return new WaitForSeconds(5);
target.powerup = 0;
}
and then do
private void OnTriggerEnter(Collider collision)
{
if(collision.TryGetComponent<Player>(out var player))
{
player.StartCoroutine(PowerUpRoutine(player));
Destroy(gameObject);
}
else if(collision.TryGetComponent<Enemy>(out var enemy))
{
enemy.StartCoroutine(PowerUpRoutine(enemy));
Destroy(gameObject);
}
}
Note: Coroutines are also stopped when the MonoBehaviour is destroyed or if the GameObject the MonoBehaviour is attached to is disabled.
You should destroy the gameobject when the coroutine finishes.
StartCoroutine(removePower(collision.gameObject.GetComponent<enemy>()));
//Destroy(gameObject);
IEnumerator removePower(GameObject target) {
yield return new WaitForSeconds(5);
target.powerup = 0;
Destroy(gameObject);
}
Or run the coroutine on another gameobject
var enemy = collision.gameObject.GetComponent<enemy>();
enemy.StartCoroutine(removePower(enemy));
Related
I'm trying to make a simple damage boost I have 2 scripts, one attached to the projectile and one to the booster
Projectile:
public class DamageDealer : MonoBehaviour
{
[SerializeField] public int damage = 25;
public int GetDamage()
{
return damage;
}
public void Hit()
{
Destroy(gameObject);
}
public void IncreaseDamage(int value)
{
damage += value;
Mathf.Clamp(damage, 0, int.MaxValue);
}
}
Booster:
public class DamageUp : MonoBehaviour
{
int damageBoost = 50;
DamageDealer damageDealer;
void Awake()
{
damageDealer = FindObjectOfType<DamageDealer>();
Debug.Log(damageDealer); // this returns as null
}
private void OnTriggerEnter2D(Collider2D other)
{
if (other.tag == "Player")
{
damageDealer.IncreaseDamage(damageBoost);
}
}
}
As you can see in the Booster I've already identified what's returning as null, but I'm quite new to unity and C# so I don't know what I'm doing wrong
Some more info the Projectile is a prefab while the Booster is not (yet)
scripts are attached and enabled, objects are enabled too.
Hit() and GetDamage() are for another script I have no issue with.
Thanks in advance.
You're probably trying to find the object before it's instantiated, change the Awake to Start or try to change Script Execution Order settings
Check Also; Order of execution for event functions
I am trying to set a gameobject to active on collision, wait a second and then set it to inactive again, but after the WaitForSeconds() line the execution seems to stop. I have no experience with C# and Unity so this may be a beginner mistake. Any idea why this could be happening?
private IEnumerator OnCollisionEnter2D(Collision2D collidedWith) {
if (collidedWith.gameObject.tag == "Shape") {
collidedWith.transform.GetChild(0).gameObject.SetActive(true);
Object.Destroy(this.gameObject);
yield return new WaitForSeconds(1);
Debug.Log("Should have waited for 1 second");
collidedWith.transform.GetChild(0).gameObject.SetActive(false);
}
}
You do
Object.Destroy(this.gameObject);
on this object which is running the Coroutine -> The routine is interrupted in that very same moment (when it reaches the first yield statement) -> it never actually starts to wait ;)
You should rather have a component on the object you collide with and make sure the Coroutine is run on that one instead.
E.g. like
// Have this on your Shape objects
public class Shape : MonoBehaviour
{
private bool isCollided;
public void Collided()
{
if(!isCollided) StartCoroutine(Routine());
}
private IEnumerator Routine()
{
if(isCollided) yield break;
isCollided = true;
var child = transform.GetChild(0).gameObject;
child .SetActive(true);
yield return new WaitForSeconds(1);
child.SetActive(false);
isCollided = false;
}
}
and then rather do e.g.
private void OnCollisionEnter2D(Collision2D collidedWith)
{
if (collidedWith.gameObject.TryGetComponent<Shape>(out var shape))
{
shape.Collided();
Destroy(gameObject);
}
}
I am trying to implement a waves of enemies, where you need to create a new wave when the current wave has been destroyed.
Wave Code
[SerializeField] List<WaveConfig> waveConfigs;
[SerializeField] int startingWave = 0;
WaveConfig waveConfig;
void Start()
{
StartCoroutine(SpawnAllEnemyInWawe());
}
IEnumerator SpawnAllEnemyInWawe()
{
for(int waweIndex = startingWave; waweIndex < waveConfigs.Count; waweIndex++)
{
var currentWave = waveConfigs[waweIndex];
yield return StartCoroutine(SpawnAllEnemyInWawe(currentWave));
}
}
IEnumerator SpawnAllEnemyInWawe(WaveConfig waveConfig)
{
for(int enemyCount = 0; enemyCount<waveConfig.GetEnemyNumberOfParh(); enemyCount++)
{
GameObject newEnemy = Instantiate(waveConfig.GetEnemyPrefabs(),
waveConfig.GetEnemyWaiponts()[0].transform.position,
Quaternion.identity);
newEnemy.GetComponent<EnemyPath>().SetWaveConfig(waveConfig);
yield return new WaitForSeconds(waveConfig.GetTimeMegduSpawnomEnemy());
}
}
If I understand your point correctly.
this code not help you to wait previous wave was eliminate, it work flow is spawn next wave instantly after previous wave spawn completed.
IEnumerator SpawnAllEnemyInWawe()
{
for(int waweIndex = startingWave; waweIndex < waveConfigs.Count; waweIndex++)
{
var currentWave = waveConfigs[waweIndex];
yield return StartCoroutine(SpawnAllEnemyInWawe(currentWave));
}
}
IF you need to wait until current wave eliminate.
When wave begin, you should track all enemies in the wave (eg. amount, alive data, or something) and check it for start next wave.
Psudo :
void Update(){
if(enemyInWave == 0){ // Or Condition is fullfill to start next wave
NextWave();
}else {
// Do Nothing
}
}
void NextWave(){
var waveConfig = WaveConfig[currentWaveIndex]
enemyInWave = waveConfig.monsters.Amount
SpanwEnemy(waveConfig.monsters)
}
or something like that
You could use a static HashSet/List where you store all instances of created enemies. The enemies simply add and remove themselves to/from the HastSet during their lifetime.
Then you can simply wait until no enemies are left before going into the next wave.
Something like
public class YourWaveClass : MonoBehaviour
{
[SerializeField] List<WaveConfig> waveConfigs;
[SerializeField] int startingWave = 0;
private static HastSet<Enemy> currentlyAliveEnemies = new HashSet<Enemy>();
public static void Add(Enemy enemy)
{
if(!currentlyAliveEnemies.Contains(enemy)) currentlyAliveEnemies.Add(enemy);
}
public static void Remove(Enemy enemy)
{
if(currentlyAliveEnemies.Contains(enemy)) currentlyAliveEnemies.Remove(enemy);
}
void Start()
{
StartCoroutine(SpawnAllEnemyInWawe());
}
IEnumerator SpawnAllEnemyInWawe()
{
for(int waweIndex = startingWave; waweIndex < waveConfigs.Count; waweIndex++)
{
var currentWave = waveConfigs[waweIndex];
yield return StartCoroutine(SpawnAllEnemyInWawe(currentWave));
}
}
IEnumerator SpawnAllEnemyInWawe(WaveConfig waveConfig)
{
for(int enemyCount = 0; enemyCount<waveConfig.GetEnemyNumberOfParh(); enemyCount++)
{
// NOTE: It would be better/saver if you make your prefab directly of type Enemy then
var newEnemy = Instantiate(waveConfig.GetEnemyPrefabs(), waveConfig.GetEnemyWaiponts()[0].transform.position, Quaternion.identity);
newEnemy.GetComponent<EnemyPath>().SetWaveConfig(waveConfig);
yield return new WaitForSeconds(waveConfig.GetTimeMegduSpawnomEnemy());
}
// Now wait until the HashSet is empty => all enemies are dead
yield return new WaitUntil(() => currentlyAliveEnemies.Count == 0);
}
}
So what you need to have on your enemy prefabs would be a component like
public class Enemy : MonoBehaviour
{
// This is automatically called during Instantiate
private void Awake()
{
YourWaveClass.Add(this);
}
// this is called automatically when your object gets destroyed/dies
private void OnDestroy()
{
YourWaveClass.Remove(this);
}
}
I have a GameObject that I am spawning from a pool, that when I try to return it to the pool I get the error:
Setting the parent of a transform which resides in a prefab is
disabled to prevent data corruption.
I have used my ObjectPooler in many projects and have never once encountered this error.
I have created simple test GameObjects and it still throws this error, using the below code.
I have a Bomb abstract class. I have a PowerBomb class that inherits from Bomb. Bomb conforms to an interface IExplodable.
When my player spawns a bomb, I put it as the child of the player object so the player can move around with the bomb.
My ObjectPool has the correct bomb gameobject prefab ( a PowerBomb ). and my player has the same prefab as a reference to spawn. They player also holds a reference so it knows what it is holding.
I have spent the better part of 2 days trying to figure out why my PowerBomb will not return to the object pool.
Any help?
Bomb.cs
public abstract class Bomb : MonoBehaviour, IExplodable {
//Interface
public virtual void OnDetonate() {
Die();
}
public virtual void Die() {
Debug.Log("Bomb Die()");
ObjectPool.instance.PoolObject(gameObject);
}
}
PowerBomb.cs
public class PowerBomb : Bomb {
[SerializeField]
private AudioClip sfxSpawn;
}
BombManager.cs
public GameObject bomb;
public void SpawnBomb(){
GameObject obj = ObjectPool.instance.GetObject(bomb);
if (obj == null) return;
obj.SetActive(true);
PlayerInstance.HoldObject(obj);
}
public void DetonateBomb(){
bomb.GetComponent<PowerBomb>().OnDetonate();
}
Player.cs
[SerializeField]
private bool isHolding = false;
public bool IsHolding {
get { return isHolding; }
set { isHolding = value; }
}
private GameObject holdingObject;
public GameObject HoldingObject {
get { return holdingObject; }
set { holdingObject = value;}
}
public void HoldObject(GameObject o) {
HoldingObject = o;
HoldingObject.transform.SetParent(transform);
HoldingObject.transform.position = holdingArea.transform.position;
IsHolding = true;
}
ObjectPool.cs
private GameObject containerObject;
private List<GameObject>[] pooledObjects;
//Code removed for brevity. Getting from the pool is fine.
public void PoolObject(GameObject obj) {
for (int i = 0; i < objectsToPool.Count; i++) {
if (objectsToPool[i].objectToPool.name.Equals(obj.name)) {
obj.SetActive(false);
//THIS LINE IS THROWING THE ERROR
obj.transform.SetParent(containerObject.transform);
pooledObjects[i].Add(obj);
return;
}
}
}
You don't and can't modify a prefab. It might work in the Editor if you use some Editor API to modify it but it won't during run-time and it won't change the changes. You are getting this error because obj is a prefab which you are trying to modify at obj.transform.SetParent(containerObject.transform);.
You don't even use prefabs in a pool. That's not normal. What you do do is use the Instantiate function to instantiate the prefab then put the instantiated object in a pool.
GameObject obj = Instantiate(prefab, new Vector3(i * 2.0F, 0, 0), Quaternion.identity);
//Add instantiated Object to your pool List
pooledObjects[i].Add(obj);
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class RedHP : MonoBehaviour
{
public float HP = 5;
public GameObject BlueWon;
public GameObject Restart;
void OnTriggerEnter2D(Collider2D trig)
{
if (trig.gameObject.tag == "ThrowableBlue")
{
StartCoroutine(BowlDestroyTime());
HP--;
if (HP <= 0)
{
BlueWon.SetActive(true);
Restart.SetActive(true);
PlayerBlueController.canMove = false;
PlayerBlueController.canFire = false;
}
}
}
IEnumerator BowlDestroyTime()
{
yield return new WaitForSeconds(1);
Destroy(trig.gameObject);
}
}
I simply want to destroy my object after too little period of time to make it look better. In IEnumerator I can't access trig.gameObject because it is defined in OnTriggerEnter2D. Is there a way to access this value?
I also tried to put IEnumerator in OnTriggerEnter2D it also didn't work. Kinda newbie
You don't have to do that. The Destroy function can take a second parameter as a delay time before the Object is destroyed.
Destroy(trig.gameObject, 1f);
If you still want to use coroutine to do this, simply make the BowlDestroyTime function to take GameObject as parameter then pass the GameObject from the OnTriggerEnter2D function to the BowlDestroyTime function to be destroyed.
void OnTriggerEnter2D(Collider2D trig)
{
if (trig.gameObject.tag == "ThrowableBlue")
{
StartCoroutine(BowlDestroyTime(trig.gameObject));
HP--;
if (HP <= 0)
{
BlueWon.SetActive(true);
Restart.SetActive(true);
PlayerBlueController.canMove = false;
PlayerBlueController.canFire = false;
}
}
}
IEnumerator BowlDestroyTime(GameObject tartgetObj)
{
yield return new WaitForSeconds(1);
Destroy(tartgetObj);
}