Unity Question: How Can I Switch From One prefab To Another With GetAxis?

Viewed 236

I want to bind the up and down arrow keys to cycle through different sprites upon being pressed. If one end is reached, it would loop back to the first sprite. I've tried using the following code:

public class PhaseChanger : MonoBehaviour
{
    // saved for efficiency
    [SerializeField]
    public GameObject prefabMoon0;
    [SerializeField]
    public GameObject prefabMoon1;
    [SerializeField]
    public GameObject prefabMoon2;
    [SerializeField]
    public GameObject prefabMoon3;
    [SerializeField]
    public GameObject prefabMoon4;

    // needed for new phase
    GameObject currentPhase;

    bool previousFramePhaseChangeInput = false;

    /// <summary>
    /// Start is called before the first frame update
    /// </summary>
    void Start()
    {
        currentPhase = Instantiate<GameObject>(prefabMoon0);
    }

    /// <summary>
    /// Update is called once per frame
    /// </summary>
    void Update()
    {
        // change phase on up arrow or down arrow
        if (Input.GetAxis("ChangePhase") > 0)
        {
            // only change phase on first input frame
            if (!previousFramePhaseChangeInput)
            {
                previousFramePhaseChangeInput = true;

                // Save current position and destroy current phase
                Destroy(currentPhase);

                // instantiate next phase
                if (currentPhase = prefabMoon0)
                {
                    currentPhase = Instantiate(prefabMoon1);
                }
                else if (currentPhase = prefabMoon1)
                {
                    currentPhase = Instantiate(prefabMoon2);
                }
                else if (currentPhase = prefabMoon2)
                {
                    currentPhase = Instantiate(prefabMoon3);
                }
                else if (currentPhase = prefabMoon3)
                {
                    currentPhase = Instantiate(prefabMoon4); 
            else
            {
                // no phase change input
                previousFramePhaseChangeInput = false;
            }
        }
    }
}

When I attach the script to my main camera and run it, I'm able to make a single change with the up arrow, and then nothing else happens on subsequent presses.

I feel like I'm really close to making this work, but I also may being doing the whole thing inefficiently. Help would be much appreciated, thanks!

Also: I know I said sprites in my post and am sharing a script that calls on prefabs. I didn't know how to approach this using just the sprites without making a prefab for each. Is it possible to do this without separate prefabs for each sprite?

1 Answers

Problems

  • First of all you are using assignments

    currentPhase = XY
    

    where you should be using

    currentPhase == XY
    

    The reason why it still compiles is the implicit conversion operator for UnityEngine.Object -> bool. Basically your assigning equals writing

    currentPhase = XY;
    if(currentPhase)
    
  • It won't work like this either way because you are using Instantiate to create a new clone of a prefab which will of course have a different reference than the original prefab it was cloned from.

    So even if your checks where looking like

    if(currentPhase == XY)
    

    they will ever be true.

Solution

Instead of checking for reference equality I would rather store all prefabs/instances in an array

public GameObject[] phases;

and then simply have an int index for this array so you can simply move to the next element from the array by increasing the index.

private int currentPhase;

And you can increase it and make it wrap around using e.g.

currentPhase = (currentPhase + 1) % phases.Length;

so it will always grow from 0 up to phases.Length - 1 and then start over from 0 again.

And then I don't know the exact requirements of your use case but I would suggest to rather not all the time use Instantiate and Destroy but rather have already all the objects as instances under your object and just (de)actívate them!

you could do this like e.g.

public GameObject[] phases;
private int currentPhase;

private void Awake ()
{
    Init();
}

private void Update ()
{
    if (Input.GetAxis("ChangePhase") > 0)
    {
        if (!previousFramePhaseChangeInput)
        {
            previousFramePhaseChangeInput = true;

            NextPhase();
        }
    }
    else
    {
        previousFramePhaseChangeInput = false;
    }
}

// Disables all phases except the first one and sets the current index to 0
private void Init()
{
    for(var i = 1; i < phases.Length; i++)
    {
        phases[i].SetActive(false);
    }

    phases[0].SetActive(true);
    currentPhase = 0;
}

// Disables the current phase and enables the next one
// wraps around at the end of the array
public void NextPhase()
{
    phases[currentPhase].SetActive(false);

    // increase the counter and wrap around at the end of the array
    currentPhase = (currentPhase + 1) % phases.Length;

    phases[currentPhase].SetActive(true);
}

If you still want to Instantiate the objects because having them already in the scene is no option (for whatever reason) you could do it right before calling Init like e.g.

public GameObject[] phasePrefabs;

private GameObject[] phases;

private void Awake ()
{
    var amount = phasePrefabs.Length;
    phases = new GameObject [amount];

    for(var i = 0; i < amount; i++)
    {
        phases[i] = Instantiate(phasePrefabs[i]);
    }

    Init();
}

Though as said I would prefer to already have them right away as this is way less error prone ;)

Related