0
\$\begingroup\$

I want to call things in a specific order. I'm trying to lay the foundation for my online game. Each player will have its own game object ("avatar"), which is instantiated at runtime. Each of these avatars use prefabs and have their own components.

The prefabs contain items and also styles such as gender, facial hair, etc. So my equipment manager will need to check for default style prefabs if the item slot is empty.

Please correct me if I'm going about this the wrong way.

I have 2 issues.

First off, I'll show you the code.

CreateAvatarSceneManager is attached to its own game object in the scene:

public class CreateAvatarSceneManager : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        // inst avatar with position & scale
        GameObject avatar = Resources.Load<GameObject>("Avatar");
        avatar = Instantiate(avatar, new Vector3(-5.5f, -2.5f, 0), Quaternion.Euler(new Vector3(0, 140, 0)));
        avatar.transform.localScale = new Vector3(4, 4, 4);

        // set gender
        avatar.GetComponent<AvatarStyle>().gender = AvatarGender.Female;

        // render avatar
        avatar.GetComponent<AvatarManager>().Render();
    }
}

AvatarManager is attached to the avatar prefab that is instantiated via the scene manager above.

public class AvatarManager : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        Unrender();
    }

    // undrender all prefabs
    public void Unrender()
    {
        Debug.Log("called unrender");

        foreach (Renderer renderer in gameObject.GetComponentsInChildren<Renderer>())
        {
            renderer.enabled = false;
        }
    }

    // render default style prefabs
    public void Render()
    {
        Debug.Log("called render");

        AvatarStyle avatarStyle = gameObject.GetComponent<AvatarStyle>();
        EquipmentManager equipmentManager = gameObject.GetComponent<EquipmentManager>();

        Debug.Log(avatarStyle.gender);
        Debug.Log(equipmentManager.defaultPrefabs);

        /*
        foreach (Renderer renderer in gameObject.GetComponentsInChildren<Renderer>())
        {
            if (equipmentManager.defaultPrefabs[avatarStyle.gender].ContainsValue(renderer.name))
            {
                renderer.enabled = true;
            }
        }
        */
    }
}

AvatarStyle is also attached to the avatar prefab that is instantiated.

public class AvatarStyle : MonoBehaviour
{
    public AvatarGender gender;
}

public enum AvatarGender { Male, Female }

EquipmentManager is also attached to the avatar prefab that is instantiated.

public class EquipmentManager : MonoBehaviour
{
    public Dictionary<AvatarGender, Dictionary<EquipmentSlot, string>> defaultPrefabs;

    // Start is called before the first frame update
    void Start()
    {
        SetDefaultPrefabs();
    }

    // set default prefabs when item not present
    public void SetDefaultPrefabs()
    {
        Debug.Log("set default prefabs");

        defaultPrefabs = new Dictionary<AvatarGender, Dictionary<EquipmentSlot, string>>();

        Dictionary<EquipmentSlot, string> defaultMalePrefabs = new Dictionary<EquipmentSlot, string>
        {
            { EquipmentSlot.Head, "Chr_Head_Male_00" },
            { EquipmentSlot.Torso, "Chr_Torso_Male_00" },
        };
        defaultPrefabs.Add(AvatarGender.Male, defaultMalePrefabs);

        Dictionary<EquipmentSlot, string> defaultFemalePrefabs = new Dictionary<EquipmentSlot, string>
        {
            { EquipmentSlot.Head, "Chr_Head_Female_00" },
            { EquipmentSlot.Torso, "Chr_Torso_Female_00" },
        };
        defaultPrefabs.Add(AvatarGender.Female, defaultFemalePrefabs);
    }
}

public enum EquipmentSlot { Head, Torso }

Issue 1

It seems like my methods aren't being called in the order I want them to. If I look at the console, it is showing me this:

called render
Female
Null
called unrender
set default prefabs

You can see the Null is causing problems with the AvatarManager render function. Since its trying to call render BEFORE the default prefabs are even set, it causes an error.

I want the order to actually be more like this:

called unrender
set default prefabs
Female
(dictionary of default prefabs)
called render

Why are things firing in this order? Should it not be calling Start() BEFORE any other function call?

Issue 2

OK maybe this is actually related to issue 1. Should I actually be using singletons for the managers? If so, how do I use singletons per avatar that is instantiated?

Also, if you look at the way I am doing things, I want to turn this:

// from scene manager
avatar.GetComponent<AvatarStyle>().gender = AvatarGender.Female;

// from avatar manager
if (gameObject.GetComponent<AvatarStyle>().gender) DoStuff();

Into something like this:

// from scene manager
avatar.AvatarStyle.gender = AvatarGender.Female;

// from scene manager
if (gameObject.AvatarStyle.gender) DoStuff();

I'm more concerned with issue #1 right now. Thanks for your time and help.

\$\endgroup\$
  • \$\begingroup\$ It's usually a good idea to try to create an architecture which allows for events to happen in any order. I made the experience that it results in a design which is a lot more robust, reliable and modular. For example, the Update method of your Avatar could check if any of its visual properties was changed since the last update and then trigger its reconstruction itself. \$\endgroup\$ – Philipp Aug 13 at 11:54
1
\$\begingroup\$

Should it not be calling Start() BEFORE any other function call?

No, that's not the promise the engine makes to you. Consulting the docs:

Start is called on the frame when a script is enabled just before any of the Update methods are called the first time.

It promises you that it will call ScriptA.Start() before it, the engine, calls ScriptA.Update() or ScriptA.FixedUpdate() or ScriptA.LateUpdate(). That's all.

If you call ScriptB.Foo() inside ScriptA.Start(), the engine does not promise to jump in and interrupt this call that you made to insert a call to ScriptB.Start() before continuing to run the code you wrote.

The problem looks to be here in CreateAvatarSceneManager:

void Start()
{
    // inst avatar with position & scale
    GameObject avatar = Resources.Load<GameObject>("Avatar");
    avatar = Instantiate(avatar, new Vector3(-5.5f, -2.5f, 0), Quaternion.Euler(new Vector3(0, 140, 0)));
    avatar.transform.localScale = new Vector3(4, 4, 4);

    // set gender
    avatar.GetComponent<AvatarStyle>().gender = AvatarGender.Female;

    // render avatar
    avatar.GetComponent<AvatarManager>().Render();
}

Here you know that the avatar's components have had a chance to be instantiated, Awake(), and had OnEnable() called if enabled, as part of the Instantiate() function. Their Start() methods won't be called until after the currently-executing Start() function returns control back to the engine to continue ticking through its queue of objects that need to be started.

So, if you need something to run as part of the "construction" of the object, before it's returned to the script that spawned it, put it in Awake(). Or you can create a custom initialization method and call it yourself.

how do I use singletons per avatar that is instantiated?

This is a contradiction in terms. I recommend reviewing what a singleton is and why we use it / when to avoid it. A singleton by definition is singular - you can have only one singleton of a type active at a time. This is the main restriction of singletons and one reason why we use them with caution, only on things that logically must be singular/centralized (like the current game rules, or asset cache), and not on things that we might someday want two of (like player characters, or any system that each player in multiplayer needs "their" version/view of)

There's nothing wrong with your use of GetComponent() here - so don't make problems where you don't have them. If you find you're calling GetComponent frequently, you can cache the reference in a member variable. If you find it inelegant, you can return the first component you want directly from Instantiate() by making the prefab field the same type as the component you want to access, and exposing subsequent components through that component's members.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.