0
\$\begingroup\$

This is my GameController GameObject in the Inspector (Ignore Suns and Player). Note that the GameController script has GameSpawner as a SerializeField.

GameController GameObject

And my script for GameController below.

public class GameController : MonoBehaviour
{
    public GameObject sunParent;
    public GameObject playerPrefab;

    [SerializeField]
    GameObject sunSpawnerObject;

    SunSpawner sunSpawner;

    void Start()
    {
        sunSpawner = sunSpawnerObject.GetComponent<SunSpawner>();
    }

    // this is only for testing
    void LateUpdate()
    {
        SpawnSunAtY(sunParent, -3.5f);
        SpawnSunAtY(sunParent, 0f);
        SpawnSunAtY(sunParent, 3.5f);
    }

    void SpawnSunAtY(GameObject parent, float y)
    {
        sunSpawner.SpawnSunAtY(parent, y);
    }

}

When the Scene starts, it should get the SunSpawner script from sunSpawnerObject. Then what is SunSpawner? See below.

SunSpawner GameObject

This GameObject(prefab) has SunSpawner script as a component. And below the script for SunSpawner. (where my question begins)

public class SunSpawner : MonoBehaviour
{
    const float SUN_MIN_SIZE_FACTOR = 0.05f;
    const float SUN_MAX_SIZE_FACTOR = 0.3f;

    [SerializeField]
    GameObject sunPrefab;

    float playerRawRadius;
    float sunRawRadius;

    void Start()
    {
        // these lines work just fine
        GameObject player = GameObject.FindGameObjectWithTag("Player");
        playerRawRadius = player.GetComponent<Renderer>().bounds.size.x / 2f;
        sunRawRadius = sunPrefab.GetComponent<Renderer>().bounds.size.x / 2f;
    }

    public void SpawnSunAtY(GameObject parent, float y)
    {
        float sizeFactor = Random.Range(SUN_MIN_SIZE_FACTOR, SUN_MAX_SIZE_FACTOR);

        Vector3 position = new Vector3(GetRandomXPos(sizeFactor), y, 0);
        GameObject sun = Instantiate(sunPrefab, position, Quaternion.identity);
        sun.transform.localScale *= sizeFactor;
        sun.transform.parent = parent.transform;
    }

    float GetRandomXPos(float sizeFactor)
    {
        float offset = Camera.main.WorldToScreenPoint(new Vector3(GetHorizontalOffsetFromScreen(sizeFactor), 0, 0)).x;
        float screenRightBoundWorldPoint = Camera.main.ScreenToWorldPoint(new Vector3(Screen.width, 0, 0)).x;
        float screenLeftBoundWorldPoint = Camera.main.ScreenToWorldPoint(new Vector3(0, 0, 0)).x;

        return Camera.main.ScreenToWorldPoint(new Vector2(Random.Range(offset, screenRightBoundWorldPoint - offset), 0)).x;
    }   

    float GetHorizontalOffsetFromScreen(float sizeFactor)
    {
        float horizontalMargin = 0.1f; //TODO temporary value
        float playerRadius = playerRawRadius;  // *look here (1)*
        float distancePlayerToSun = 0.5f; //TODO temporary value
        float randomRadius = GetRandomRadius(sizeFactor);

        return horizontalMargin + (2f * playerRadius) + distancePlayerToSun + randomRadius;
    }

    float GetRandomRadius(float sizeFactor)
    {
        // *look here (2)*
        print("sunRawRadius: " + sunRawRadius + ", sizeFactor: " + sizeFactor + ", scaledRadius: " + (sunRawRadius * sizeFactor)); 
        return sunRawRadius * sizeFactor;
    }
}

Call stack:

GameController.LateUpdate() -> SunSpawner.SpawnSunAtY(parent, y) -> SunSpawner.GetRandomXPos(sizeFactor) -> GetRandomXPos(sizeFactor) -> GetHorizontalOffsetFromScreen(float sizeFactor) -> GetRandomRadius(sizeFactor)

Look here (1):

in SunSpawner.Start(), all values are initialized properly, I can see values in the debugger or log. However, when I get to GetHorizontalOffsetFromScreen(..), I see playerRawRadius is now zero.

Look here (2):

Same thing. sunRawRadius is 0, but sizeFactor argument is properly passed down the call stack.

Question: Why is it doing this?

Follow up: How do I properly instantiate prefabs through game object chains and use them?

Big thank you in advance.

\$\endgroup\$
7
  • 1
    \$\begingroup\$ You said the SunSpawner object is a prefab? Is it still sitting in your Assets folder, or do you at some point place / Instantiate a copy of it into your scene? Only copies in the scene get their Start() method called - the prefab will still have its default values. \$\endgroup\$
    – DMGregory
    Apr 3, 2019 at 11:18
  • 3
    \$\begingroup\$ Also pro tip, you can just make a public SunSpawner field and drag a gameobject with that script on it as a reference, and you'll avoid the GetComponent call. \$\endgroup\$
    – gjh33
    Apr 3, 2019 at 13:25
  • \$\begingroup\$ @DMGregory did not know the ones on the scene get Start() method called. So instead of getting the component, should I Instantiate() ? \$\endgroup\$ Apr 4, 2019 at 5:24
  • 1
    \$\begingroup\$ "I don't want that object exposed to other classes" Then use a private, [SerializeField] to get it in the inspector the same way you're doing now. gjh33's point was not about using public over private, but about using SunSpawner over GameObject type for your SunSpawnerObject field. \$\endgroup\$
    – DMGregory
    Apr 4, 2019 at 8:38
  • 1
    \$\begingroup\$ Exactly right. You can make a direct reference in the unity editor to the script itself. Then you don't have to call GetComponent. \$\endgroup\$
    – gjh33
    Apr 4, 2019 at 14:19

1 Answer 1

2
\$\begingroup\$

Like a lot of problems I think this stems from a misunderstanding of prefabs and a slight design issue. What you're looking to do is essentially make a manager type class, that is spawning and managing instances of suns. If this sun spawner is only in one scene, then you should simply have an instance of that prefab in your scene and link it from that instance. Prefabs not in a scene are just templates. They don't actually run or execute. If you need your sun spawner across multiple scenes then I will demonstrate a good setup for that.

Let's start with a unity Singleton pattern. This pattern is useful for global systems that have exactly one instance. While in other coding environments singletons are often frowned upon, they tend to make a lot of sense in game environments. Just don't go overboard.

public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
  public static T Instance { get; private set; }

  // Checks if we already have an instance, if so
  // We don't need this gameObject
  protected virtual void Awake() {
    if (instance != null) {
      Destroy(gameObject);
      return;
    }
    Instance = this
  }

  protected virtual void OnDestroy() {
    if (Instance == this) Instance = null;
  }
}

Expanding on this we have a singleton that will persist across multiple scenes if we want

public class PersistentSingleton<T> : Singleton<T>
{
  protected override void Awake() {
    base.Awake();
    DontDestroyOnLoad(gameObject);
  }
}

Finally let's make a prefab with the following script on the root

// An empty persistent singleton which will ensure only one instance
// of itself is ever created, and it will persist across scene changes
public class PersistentEntities : PersistentSingleton<PersistentEntities>
{
}

We'll call that prefab we made PersistentEntities, and within it you can put all your systems you wish to persist across multiple scenes. In each and every scene you should place an instance of this prefab. Since it persists across scenes, when it loads to another scene, the PersistentEntities instance in the new scene will see that an instance already exists via the Singleton pattern, and destroy itself. Now you have the tools to do with as you wish. If your SunSpawner is only in one scene, or only a few scenes, then maybe you should just make a SunSpawner prefab with a SunSpawner script that inherits from Singleton. If you need the sunspawner in multiple consecutive scenes and it should maintain it's state across scenes, then you could put SunSpawner inside your PersistentEntities prefab.

Hopefully these tools can help you resolve your issues. But you should decide what your SunSpawner is. Does it need to maintain information across scenes? Put it in PersistentEntitites and make it Singleton. Is it a system that should only have one instance, but it's information doesn't need to be across scene? Make it Singleton, then put an instance in the scenes you need it. Do scenes have multiple SunSpawners? Then don't make it either, and you can put multiple instances in your scene, or Instantiate them from a prefab before using them.

\$\endgroup\$
2
  • \$\begingroup\$ At the moment I'm making it so that the SunSpawner will be needed in a single scene, so I wouldn't necessarily need a Singleton instance for it, however, I will refer back to this answer when I need one! Thanks for the answer. I think I got the idea of prefabs from "Prefabs not in a scene are just templates" :) \$\endgroup\$ Apr 5, 2019 at 5:59
  • \$\begingroup\$ Glad I could help :). I usually have this singleton pattern in every project since it usually comes in handy at least once! \$\endgroup\$
    – gjh33
    Apr 5, 2019 at 13:26

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .