Search code examples
c#unity-game-enginebuttonparent-childinstantiation

Making Buttons Instantiated at run-time via script have onClick functions declared correctly


In my program I am instantiating buttons at run-time. These buttons are being instantiated to a canvas. They should reference a main object, a car. The buttons should refer to all the parts of the car. When I click a button, I want the camera to zoom in on the correct car part. The buttons have the same text as the car parts. Since they are being instantiated from a button prefab in assets at run-time, I don’t know how to code it where if I click on it, it zooms in on the correct part.

My first attempt was to attach a script to the button prefab that when clicked it would read its own text (because it grabs the text from all the children in another script) then it would read all the children text, and compare and find where the button text was the same as the children part text, then it would grab that child GameObject and it would reference that GameObject’s transform and transform the camera to the location.

I can transform the camera and everything because I have already done this by using raycast to click the car part, but I just don’t know how to assign onClick to the buttons. The code needs to be reusable so I was trying to design it where it could be used for any object ...


Picture of the inspector car and parts Picture of inspector and script that is instantiating the buttons

Below is my attempt to solve this problem. This script is attached to the button prefab...

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

public class ifClickedZoomy : MonoBehaviour
{
    public GameObject objofparts;
    public GameObject cm;

    Button btn;
    string nameofobj;

    void start()
    {


        btn = this.GetComponent<Button>();
        btn.onClick.AddListener(going);
        btn.name = nameofobj;

        var temp = objofparts.gameObject.transform.Find(nameofobj);

        //btn.onClick.AddListener(delegate { Zoomy(temp); });
    }

    public void Zoomy(Transform target)
    {

        cm.transform.position = target.transform.position + Vector3.one * 0.5f;
        cm.transform.LookAt(target);
    }

    void going()
    {
        /*
        if(btn.GetComponentInChildren<Text>().text == "SkyCar")
        {
            GameObject target = GameObject.Find("SkyCar");
            Vector2 targpos = target.transform.position;

            cm.transform.position = targpos;
        }
        */
    }

}

Solution

  • Your button prefab could have a script already attached to the button object. In this script you would have a public method called ClickBehaviour() that is pre linked to The OnClick.

    This script should have a delegate method that can be set externally from a method called SetClickBehaviour.

    A delegate is basically a variable that stores a function with a particular signature. So a delegate void is storing a void function. You can set a delegate to be a particular function. When you call the delegate as if it was a real function, it actually calls the function it is storing. In this way you can swap which function actually gets called at runtime by changing which function is stored in the delegate.

    So, you would instantiate the button prefab, then send it the behaviour to link with it by calling newButton.SetClickBehaviour(methodToCall);

    So SetClickBehaviour is responsible for changing what is stored in the delegate..

    Then inside ClickBehaviour, you would call the delegate method that was set externally.

    So ClickBehaviour actually calls that delegate.

    Button Spawner

    using UnityEngine;
    
    public class ButtonSpawner : MonoBehaviour
    {
    
        // Button Prefab
        [SerializeField] 
        RuntimeButton RuntimeButtonPrefab = default;
    
        // Where to create instantiated buttons
        [SerializeField] 
        Canvas ParentCanvas = default;
    
        void Start()
        {
            //Create Buttons
            RuntimeButton newRuntimeButton1 = Instantiate(RuntimeButtonPrefab, ParentCanvas.transform);
            newRuntimeButton1.SetClickBehaviour(MethodThatButton1ShouldDo);
    
            RuntimeButton newRuntimeButton2 = Instantiate(RuntimeButtonPrefab, ParentCanvas.transform);
            newRuntimeButton2.SetClickBehaviour(MethodThatButton2ShouldDo);
        }
    
        //These methods could be anything and from any other scripts
        public void MethodThatButton1ShouldDo()
        {
            Debug.Log("Method for Button1 Worked!");
        }
    
        public void MethodThatButton2ShouldDo()
        {
            Debug.Log("Method for Button2 Worked!");;
        }
    
    
    }
    

    Button Script

    using System;
    using UnityEngine;
    
    //This class is attached to button
    public class RuntimeButton : MonoBehaviour
    {
        //An action is just a kind of delegate.
        Action clickBehaviour;
    
        //This is called from another script to define what method gets called when button is clicked.
        public void SetClickBehaviour(Action someMethodFromSomewhereElse)
        {
            clickBehaviour = someMethodFromSomewhereElse;
        }
    
        // This is linked in button inspector to the OnClick event.
        public void ClickBehaviour()
        {
            clickBehaviour.Invoke();
        }
    
    } 
    

    In Action: enter image description here

    One thing to note as well: in your updated question, your script is in charge of several things: making the button listen, finding a part, listening for clicks, moving the cameras, and "going". That's way too much for one script to handle! It's a good idea to parse these ideas out into different scripts, each with only one main responsibility. I find this often helps diagnose problems, and can suggest possible solutions. In my example scripts, RuntimeButton is only responsible for capturing an OnClick, and calling its delegate. It doesn't care what that delegate does.

    ButtonSpawner is only in charge of spawning buttons. In a real program, the two methods that get passed to the buttons would definitely reside in separate scripts, since they have nothing to do with spawning buttons. I just put them in there for this minimal example.