MarkHughes88
MarkHughes88

Reputation: 669

Can only spawn one object at once in unity

In my game I have a game object called ExclamationMark which I want to spawn above enemies heads when the player gets into range and they become "Alerted".

I've made this simple script to do that, but for some reason it will only work on one game object.

My enemy script:

void CheckForPlayer()
{
    // Define player and get position
    var player = GameObject.FindWithTag("Player");
    var playerPos = (int)player.transform.position.x;

    if (transform.Find("Graphics"))
    {
        // Define gameobject position
        var enemyPos = transform.Find("Graphics").gameObject.transform.position.x;

        // Define range to spawn tiles in
        var range = 5;
        var rangeInfront = enemyPos + range;
        var rangeBehind = enemyPos - range;

        if (playerPos >= rangeBehind && playerPos <= rangeInfront)
        {
            enemyIsActive = true;

            if (transform.Find("ExclamationMark"))
            {
                var exMark = transform.Find("ExclamationMark").gameObject.GetComponent<ExclamationMarkSpawn>();

                exMark.SpawnExclamationMark();
            }
        }
        else
        {
            enemyIsActive = false;
        }
    }
}

My ! script:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ExclamationMarkSpawn : MonoBehaviour {

    public GameObject spawnPos;
    public GameObject exclamationMark;
    public GameObject exclamationMarkAudio;

    public void SpawnExclamationMark()
    {
        StartCoroutine(GameObject.FindGameObjectWithTag("MainCamera").GetComponent<CameraShake>().Shake(0.2f, 0.2f, 0.2f));
        Instantiate(exclamationMark, spawnPos.transform.position, Quaternion.identity);
        if (exclamationMarkAudio)
            Instantiate(exclamationMarkAudio, spawnPos.transform.position, Quaternion.identity);
        StartCoroutine(DestroyExclamationMark());
    }

    IEnumerator DestroyExclamationMark()
    {
        yield return new WaitForSeconds(1);
        var children = new List<GameObject>();
        foreach (Transform child in transform) children.Add(child.gameObject);
        children.ForEach(child => Destroy(child));
    }
}

Upvotes: 1

Views: 708

Answers (2)

FaTaLL
FaTaLL

Reputation: 511

Try this;

if (transform.Find("ExclamationMark"))
{
    var exMark = transform.Find("ExclamationMark").gameObject.GetComponent<ExclamationMarkSpawn>();        
    exMark.SpawnExclamationMark(transform.position); //Add transform.position here
}

public void SpawnExclamationMark(Vector3 EnemyPos)
{
    StartCoroutine(GameObject.FindGameObjectWithTag("MainCamera").GetComponent<CameraShake>().Shake(0.2f, 0.2f, 0.2f));
    Instantiate(exclamationMark, EnemyPos, Quaternion.identity);
    if (exclamationMarkAudio)
        Instantiate(exclamationMarkAudio, EnemyPos, Quaternion.identity);
    StartCoroutine(DestroyExclamationMark());
}

Upvotes: -1

derHugo
derHugo

Reputation: 90590

Just to be sure: I assume every player has its own instance of both of your scripts attached (some maybe nested further in their own hierarchy).

I assume that since you are using transform.Find which looks for the object by name within it's own children.


In general using Find and GetComponent over and over again is very inefficient! You should in both classes rather store them to fields and re-use them. Best would be if you can actually already reference them via the Inspector and not use Find and GetComponent at all.

In general finding something by name is always error prone. Are you sure they are all called correctly? Or are others maybe further nested?

Note: Find does not perform a recursive descend down a Transform hierarchy.

I would prefer to go by the attached components. You say it has e.g. a RigidBody. If this is the only Rigidbody component in the hierarchy below your objects (usually this should be the case) then you could instead rather simply use

// pass in true to also get disabled or inactive children
Rigidbody graphics = GetComponentInChildren<Rigidbody>(true);

the same for the ExclamationMarkSpawn

// Would be even beter if you already reference these in the Inspector
[SerializeField] private Rigidbody graphics;
[SerializeField] private ExclamationMarkSpawn exclamationMark;
[SerializeField] private Transform player;

private void Awake()
{
    if(!player) player = GameObject.FindWithTag("Player");
    if(!graphics) graphics = GetComponentInChildren<Rigidbody>(true);
    if(!exclamationMark) exclamationMark = GetComponentInChildren<ExclamationMarkSpawn>(true);
}

private void CheckForPlayer()
{
    // If really needed you can also after Awake still use a lazy initialization
    // this adds a few later maybe unnecessary if checks but is still 
    // cheaper then using Find over and over again
    if(!player) player = FindWithTag("Player");
    if(!graphics) graphics = GetComponentInChildren<Rigidbody>(true);
    if(!exclamationMark) exclamationMark = GetComponentInChildren<ExclamationMarkSpawn>(true);

    var playerPos = (int)player.position.x;

    // always if making such a check also give a hint that something might be missing
    if (!graphics)
    {
        // by adding "this" you can now simply click on the message
        // in the console and it highlights the object where this is happening in the hierarchy
        Debug.LogWarning("graphics is missing here :'( ", this);
        return;
    }

    // Define gameobject position
    var enemyPos = graphics.transform.position.x;

    // Define range to spawn tiles in
    // this entire block can be shrinked down to
    if (Mathf.Abs(playerPos - enemyPos) <= 5)
    {
        enemyIsActive = true;

        if (exclamationMark) exclamationMark.SpawnExclamationMark();
    }
    else
    {
        enemyIsActive = false;
    }
}

The same also in ExclamationMarkSpawn.cs.

I would additionally only allow 1 exclamation mark being visible at the same time. For example when a player jitters in the distance especially assuming both, the player and the enemy, I would move the entire instantiation to the routine and use a flag. Especially since this is called every frame in Update while the player stays in the range!

Also re-check and make sure your enemies are not maybe referencing the same spawnPos and thus all instantiating their exclamation marks on top of each other.

public class ExclamationMarkSpawn : MonoBehaviour 
{
    public Transform spawnPos;
    public GameObject exclamationMark;
    public GameObject exclamationMarkAudio;

    [SerializeField] private CameraShake cameraShake;

    // only serialized for debug
    [SerializeField] private bool isShowingExclamation;

    private void Awake()
    {
        if(!cameraShake) cameraShake = Camera.main.GetComponent<CameraShake>();

        // or assuming this component exists only once in the entire scene anyway
        if(!cameraShake) cameraShake = FindObjectOfType<CameraShake>();
    }

    public void SpawnExclamationMark()
    {
        StartCoroutine(ShowExclamationMark());
    }

    private IEnumerator ShowExclamationMark()
    {
        // block concurrent routine call
        if(isShowingExclamation) yield brake;

        // set flag blocking concurrent routines
        isShowingExclamation = true;

        // NOTE: Also for this one you might want to rather have a flag
        // multiple enemy instances might call this so you get concurrent coroutines also here
        StartCoroutine(cameraShake.Shake(0.2f, 0.2f, 0.2f));
        Instantiate(exclamationMark, spawnPos.position, Quaternion.identity);
        if (exclamationMarkAudio) Instantiate(exclamationMarkAudio, spawnPos.position, Quaternion.identity);

        yield return new WaitForSeconds(1);
        var children = new List<GameObject>();
        foreach (var child in transform.ToList()) children.Add(child.gameObject);
        children.ForEach(child => Destroy(child));

        // give the flag free
        isShowingExclamation = false;
    }
}

Upvotes: 2

Related Questions