How to access a public variable from one script by another script? The best way

I have only a basic C# course finished. And I study the in-build tutorial.
I have kind of count of collectibles and I need to play special effect on the last collectible when the count reaches zero.

So now I need to access variables from a one class to another… And there are couple ways which I can do it in C#

  1. Make a class static, so I can access directly these variables.

  2. I can instantiate a dynamic class with desired variable, but the problem I need to play visual effect on a collectible, but all logic of count stuff is in UI object…

  3. Maybe I can use generic GetComponent<T>() method somehow?

I just get frustrated by Unity codding for now, it looks to me pretty chaotic, a lot of things, hard to understand them all, and to find some solution for the problem… Thank you for the answers.
And this is the class for UI object with collectible count proposed for me in the Tutorial.

using UnityEngine;
using TMPro;
using System; // Required for Type handling

public class UpdateCollectibleCount : MonoBehaviour
{
    private TextMeshProUGUI collectibleText; // Reference to the TextMeshProUGUI component

    public int totalCollectibles;

    void Start()
    {
        collectibleText = GetComponent<TextMeshProUGUI>();
        if (collectibleText == null)
        {
            Debug.LogError("UpdateCollectibleCount script requires a TextMeshProUGUI component on the same GameObject.");
            return;
        }
        UpdateCollectibleDisplay(); // Initial update on start
    }

    void Update()
    {
        UpdateCollectibleDisplay();
    }

    private void UpdateCollectibleDisplay()
    {
        totalCollectibles = 0;

        // Check and count objects of type Collectible
        Type collectibleType = Type.GetType("Collectible");
        if (collectibleType != null)
        {
            totalCollectibles += UnityEngine.Object.FindObjectsByType(collectibleType, FindObjectsSortMode.None).Length;
        }

        // Optionally, check and count objects of type Collectible2D as well if needed
        Type collectible2DType = Type.GetType("Collectible2D");
        if (collectible2DType != null)
        {
            totalCollectibles += UnityEngine.Object.FindObjectsByType(collectible2DType, FindObjectsSortMode.None).Length;
        }

        // Update the collectible count display
        collectibleText.text = $"Collectibles remaining: {totalCollectibles}";
    }
}

P. S. I edited the proposed script so made the count variable public.

when you are simply trying to access a variable and not changing its value from another script then just make a property of that variable.

private int totalCollectables;
public int TotalCollectables => totalCollectables;

It is a weird code. I mean, the auto-generated properties to me usually looks like that.

public int TotalCollectables { get; set; }

The code like you posted, Mr. preety means we create a method and just use a lamba-expression to write the variable totalCollectables? But the question was not about how to make a property. But how to access variable from another script.

There isn’t really one best way, as the ‘best’ way is always contextual.

There’s a number of ways to get references to thing. Such as gleaming components through collisions, triggers, or the return value of Instantiate(), or a number of other ways.

There is also simply the method of referencing game objects or components via the inspector. With collectables that are probably always going to be present in a scene, this is probably the most straight forward option.

If I had a number of collectables and wanted to know when all have been collected, I might hook things up by the inspector, and use delegates to know when one gets picked up:

public class CollectableGroup : MonoBehaviour
{
    [SerializeField]
    private List<Collectable> _groupCollectables = new();
    
    private int _startingCollectableCount;
    
    public int StartingCollectableCount => _startingCollectableCount;
    
    public int RemainingCollectableCount => _groupCollectables.Count;
    
    public event Action OnAllCollectablesPickedUp;
    
    private void Awake()
    {
        _startingCollectableCount = _groupCollectables.Count;
        foreach (var collectable in _groupCollectables)
        {
            collectable.OnCollectablePickedUp += HandleCollectablePickedUp;
        }
    }
    
    private void HandleCollectablePickedUp(Collectable collectable)
    {
        collectable.OnCollectablePickedUp -= HandleCollectablePickedUp;
        _groupCollectables.Remove(collectable);
        
        if (RemainingCollectableCount == 0)
        {
            OnAllCollectablesPickedUp?.Invoke();
        }
    }
}

public class Collectable : MonoBehaviour
{
    public event System.Action<Collectable> OnCollectablePickedUp;
    
    private void OnTriggerEnter(Collider collider)
    {
        var go = collider.gameObject;
        if (go.CompareTag("Player") == true)
        {
            OnCollectablePickedUp?.Invoke(this);
            
            // spawn/play sound and fx
            
            Destroy(this.gameObject);
        }
    }
}

Notably the group has a delegate to indicate when all its collectables have been picked up. Thus, you can write any other components that hooks into this delegate.

1 Like

OMG! On the small course that I have finished, nobody explained events… Of course I have idea, but still. I am trying to understand your source code Mr. Spiney…

  1. You create list of collectibles, with the help of constructor, despite in Unity it is forbidden… I mean the operator new generates compile time error…

  2. You using shortcut lambda-expression for methods, instead of pick auto-generated property, why is so? I really do not get it…

  3. Awake() method is working on the start of the video-game, only once? As far as I understand, but the number of collectables which are picked up changes through time… So how this methods helps?

  4. Why we use OnAllCollectablesPickedUp?.Invoke() on delegate if RemainingCollectableCount is equal to zero (and actually it can be less than zero?)

I will further attempt to understand. For now I think that you used all this complicated delegate classes and events mechanims to avoid check on collectables the every frame?

Constructors are not forbidden. We only can’t/shouldn’t instance MonoBehaviours or Scriptable objects (and some other UnityEngine.Object derived types ) via a constructor.

If the = new() shortened syntax is throwing an error than you’re on an older version of Unity that doesn’t support that C# feature. You can just use the full = new List<Collectable>() instead.

Mind you, as the list is going to be serialized, you can omit the constructor entirely as well, as it will be serialized into a non-null state.

They’re still properties, just get-only properties. They’re known as expression-bodied properties: Expression-bodied members - C# | Microsoft Learn

Awake is just being used for some initial set-up that we only want to happen once. We cache the amount of starting collectables (in case something else cares about this, such as a UI element), and registering to the delegate of all the collectables referenced by this group. This is something we only want to happen once.

As it stands my code also acts as an example of how to write in a way that doesn’t need Update calls.

Like I said before, its so other objects can listen for when all the collectables in a group have been picked up. Do you can have another component play a sound effect, open a door, etc etc.

1 Like

Hey Frostysh!

Use public static event Actions!
I’ve had too many headaches to count, because of asking myself, how do I give values to other scripts?

Well I am currently using this method and I love it:

public class PlayerCollectibles : MonoBehaviour
{
    /* Events */
    public static event Action OnLastCollectiblePickedUp;

    /* Parameters */
    int amountNeeded = 10;

    private void Collect()
    {
        amountNeeded--;

        if (amountNeeded == 0)
        {
            // Invoke the event (do not forget the "?")
            OnLastCollectiblePickedUp?.Invoke();
        }
    }
}

If you work with collision, you could count collectibles like this

private void OnTriggerEnter2D(Collider2D collision)
{
    if (collision.CompareTag("Collectible"))
    {
        Collect();
    }
}

Then in some kind of effect class, you do this:

public class EffectMaster : MonoBehaviour
{
    private void OnEnable()
{
    // Subscribe to the event
    PlayerCollectibles.OnLastCollectiblePickedUp += ActivateEffect;
}

private void OnDisable()
{
    // Unsubscribe from the event
    PlayerCollectibles.OnLastCollectiblePickedUp -= ActivateEffect;
}

    private void ActivateEffect()
    {
        // Activate the effect
    }
}

Now ActivateEffect() will execute once the event is invoked, after you have collected everything. You do not need a reference to anything, this is way tider than most of what I tried.

Hope this helps!

You can also do

public static event Action<int> OnEventInvoked;

void InvokeEvent()
{
    OnEventInvoked?.Invoke(42);
}

private void OnEnable()
{
    // Subscribe to the event
    PlayerCollectibles.OnLastCollectiblePickedUp += ActivateEffect;
}

private void OnDisable()
{
    // Unsubscribe from the event
    PlayerCollectibles.OnLastCollectiblePickedUp -= ActivateEffect;
}

private void ActivateEffect(int theAnswerForEverything)
    {
        // Activate the effect
        int whatIsTheAnswerForEverything = theAnswerForEverything;
    }

btw!

1 Like

To Mr. Spiney.

About constructors, I understand, to use of the constructors is only prohibited for certain classes, like those whose derived from MonoBehaviour class. Actually I watched some classes, like the Object class in Unity, it is a pretty chaos impression I have after. It is too complicated to mineself.
I also will consider to shortcut expressions for instantiating commands.

Why do you need _startingCollectableCount variable? I mean we anyway working only with the Count variable of the List intsance.

I do not understand the next code.

foreach (var collectable in _groupCollectables)
        {
            collectable.OnCollectablePickedUp += HandleCollectablePickedUp;
        }

We have an empty List instance… Of the type of Collectable and we trying to use foreach on the empty List… The loop should subscribe the method HandleCollectablePickedUp to the corresponding delegate of the event of the each member of the List. Do I understand it right?
— As I remembered, I hope correct memory, we should use EventArgs class and EventHandler delegate to work with events, instead of this, we using a custome delegate and a method which has a parameter of Collectable type… So how we send this parameter as the argument into this method? :potable_water: I am little bit frustrated.

— In the method HandleCollectablePickedUp() we for some reason de-subscribing this very method from every Collectable which triggered the event… Why? It should cause an error of null-reference exception because of OnTriggerEnter() in Collectible has instruction Destroy(this). I will try to test this thing on my example, it somehow gets more complicated…

P. S. The Remove() method of reference data stuff is a pretty severe in terms of resources as far as I remember. It searches through the List by some crazy algorithm and tries compare the reference or hash-codes of the object. I can mistake of course, and of course, in case of 20 collectibles it is do not matter.

To Mr. Tidali.

Indeed, to manage mechanism of proper value migration from one class to other in Unity is not an easy task, as I can see. At least for me.
Action type suppose to be delegate Action from the C# vanilla? I tried to search for it and the IDE (MS Visual Studio, but I also use sometimes Rider) said that this type is from some Unity assembly called netstandard.dll… Nevermind, I will try to use this event-madness on the tutorial task. To clarify what doing the program based on your code Mr. Tidaly…

  1. We create a class with a name of PlayerCollectibles? Based on the name I should guess it is kind of a warehouse of collectibles?
    — In this class we create even of the type of Action delegate, why not EventHander
    — The value amountNeeded should represent count after which the visual effect triggers? Am I right here?

  2. OnTriggerEnter2D is a method whic works with 2D ‘enter the specific zone’ detection, as far as I understand. And by your idea, it should be placed in Player components? The tag-argument “Collectible” should help use to trigger method Collect (from PlayerCollectibles class) only when player hits the collectible?
    How we can launch this method without reference to its class? The class is not static by itself…

  3. EffectMaster should represent the script of Effect-object.
    — Then we have method OnEnable() which subscribes the ActivateEffect method to the static event of PlayerCollectibles.
    — And why wee need OnDisable() method, just curious.

I will try to comprehend all of that, what said you and Mr Spiney, and to implement the result in the cursed tutorial… :pushpin:

First, please stop worrying about performance. You’re too new to be caring about this and don’t have the experience to be reasoning about it. It’s only going to hamper your learning and slow down any work you do.

99% of the time the things newbies think are performance issues are not an issue an all.

And no List<T>.Remove is not going to be a performance issue under normal circumstances. Especially when the list is only going to have a small number of elements.

It’s a serialized list. As I mentioned you would assign references to the components in the inspector at edit time. Thus, at runtime, the collection is not empty, and we subscribe to all the collectables in the group.

We pass the argument when we invoke it in the Collectable class, with OnCollectablePickedUp?.Invoke(this);, effectively the collectable passes itself into the delegate.

As I already mentioned its so that other systems can see how many collectables we started out with. Imagine a UI element that shows how many collectables you have left or something. We need to cache and expose this information. And we expose it as a get-only property so it can only be read from, and not written to.

No it won’t cause a null-ref because we are unsubscribing before the game object gets destroyed.

Everything happens linearly. When we invoke the delegate, any subscribers will run before execution continues onwards.

Lets run through all this simply:

  1. We make a game object for a collectable group, add the component, and assign via the inspector all the collectables for this group.
  2. When we enter play mode, Awake runs and the group subscribes to the OnCollectablePickedUp delegate of each collectable, so it can listen for when each individual one is picked up.
  3. When a collectable does get picked up, it invokes its callback (before it destroys itself).
  4. Since the group is listening, it unsubscribes from the collectables callback (no need to be listening to it anymore), removes it from its group, and if the group is now empty, we invoke the OnAllCollectablesPickedUp callback, so anything else listening for the group to be finished can respond.
  5. Now that the callback has finished executing, everything else after the callback then executes, such as playing some effects and destroying the collectable.

Hope that all makes sense. The code I have posted does work. I do know what I am doing.

Remember that delegates are a C# thing and not specific to Unity. There is ample information about them available on the net.

1 Like

Recently I have read articles. And trying to understand the common and efficient way to create connection between classes. But for now I cannot make anything acceptable… I never thought about simple newbie tutorial will be so hard to me.
I am trying to not focus of efficiency in terms of resources of the program, but still… And in the same time, I have my attempts to understand Scriptable Objects.

using UnityEngine;
using System;

[CreateAssetMenu(fileName = "Count", menuName = "Collectibles/Count")]
public class CollectibleCount : ScriptableObject
{
    //[HideInInspector]
    public int count;    

    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Awake()
    {
        // Check and count objects of type Collectible
        Type collectibleType = Type.GetType("Collectible");

        if (collectibleType != null)
        {
            count = UnityEngine.Object.FindObjectsByType(collectibleType, FindObjectsSortMode.None).Length;
        }
    }
}

For now my Idea is next: use ScriptableObject instance and attach it to Collectibles, and then use the next program in Collectible script

using UnityEngine;
using UnityEngine.Audio;

public class Collectible : MonoBehaviour
{
    [SerializeField]
    private float rotationSpeed;

    [SerializeField]
    private GameObject onCollectEffect;

    [SerializeField]
    private GameObject onAllCollectEffect;

    [SerializeField]
    private CollectibleCount total;

    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        transform.Rotate(0, rotationSpeed, 0);      
    }

    public void OnTriggerEnter(Collider other)
    {
        // Destroy a collectible if collides a player

        total.count = total.count - 1;

        if (other.CompareTag("Player") && total.count == 0)
        {
            Instantiate(onAllCollectEffect, transform.position, transform.rotation);

            Destroy(gameObject);
        }
        else if (other.CompareTag("Player"))
        {
            Instantiate(onCollectEffect, transform.position, transform.rotation);

            Destroy(gameObject);
        }
    }
}

I know, it is not good in terms of organization (the collectible know about how many of them and depends on some Count), and my naming is not good, but somehow it is not working…
So how I can look on variables during Game Mode in Unity?

Try using the singleton approach with this, So then you can reference your singleton in your scripts, and then get any PUBLIC varibles from the singleton by just

Debug.Log(class.instance.myHealth); // Gets the health from the singleton

Scriptable objects do not work the same as MonoBehaviours. Your Awake method will not be called when you expect it to.

Also, side note, why are you getting the System.Type for Collectible??? FindObjectsByType has a generic version, FindObjectsByType<T>, which you should be using in almost all cases, unless you know what you’re doing.

In any case, if you want to use a scriptable object like this, you would need to register all collectables to said scriptable object, so it can manage its own count.

Perhaps like so:

[CreateAssetMenu(fileName = "Count", menuName = "Collectibles/Count")]
public class CollectibleCount : ScriptableObject
{
    private readonly List<Collectible> _registeredCollectles = new();
	
	public bool RemainingCollectibles => _registeredCollectles.Count;
	
	// invoked when any collectable from this group is picked up
	public event System.Action OnCollectiblePickedUp;
	
	// invoked when the last collectable has been picked up
	public event System.Action OnAllCollectiblesPickedUp;
	
	public void RegisterCollectible(Collectible collectible)
	{
		bool contains = _registeredCollectles.Contains(collectible);
		if (contains == false)
		{
			_registeredCollectles.Add(collectible);
		}
	}
	
	public bool UnregisterCollectible(Collectible collectible)
	{
		return _registeredCollectles.Remove(collectible);
	}
	
	public void CollectiblePickedUp(Collectible collectible)
	{
		bool removed = UnregisterCollectible(collectible);
		if (removed == true)
		{
			OnCollectiblePickedUp?.Invoke();
			
			if (RemainingCollectibles == 0)
			{
				OnAllCollectiblesPickedUp?.Invoke();
			}
		}
	}
}

Again, using delegates so other objects can listen to changes in the scriptable object.

Then you update the collectible to register/unregister itself to the total:

public class Collectible : MonoBehaviour
{
    [SerializeField]
    private float rotationSpeed;

    [SerializeField]
    private GameObject onCollectEffect;

    [SerializeField]
    private GameObject onAllCollectEffect;

    [SerializeField]
    private CollectibleCount total;

    private void Awake()
	{
		total.RegisterCollectible(this);
	}
	
	private void OnDestroy()
	{
		total.UnregisterCollectible(this);
	}

    // Update is called once per frame
    void Update()
    {
        transform.Rotate(0, rotationSpeed, 0);      
    }

    public void OnTriggerEnter(Collider other)
    {
        if (other.CompareTag("Player"))
        {
            Instantiate(onCollectEffect, transform.position, transform.rotation);
			total.CollectiblePickedUp(this); // tell the total we've been picked up
            Destroy(gameObject);
        }
    }
}
1 Like

I know a basic about some patterns, so-called patterns in programming, like Abstract Factory (building pattern), and the Singleton pattern. But the problem any derived classes from MonoBehaviour cannot use constructors directly, and in Singleton pattern I need to make the constructor private to stop the access from the outside of the class for other programmers and code.

If I fail the ScriptableObject method I will use Singleton thing.

Thank you, I have fixed it to count = UnityEngine.Object.FindObjectsByType<Collectible>(FindObjectsSortMode.None).Length generic version, I just did not explored UnityEngine.Object namespace yet, so I did not know about generic version of the method.
Something madness happens, my count starts from zero and then every interaction downgrades it by 2 instead of one, and in addition the count persist after the next start of the game mode… :face_with_peeking_eye:


I need somehow get things into order…

How to run my program step-by-step like after a breakpoint? To look at variables runtime, I tried ‘Attach to Unity’, but somehow it not works…
Collelctible count.

using UnityEngine;
using System;

[CreateAssetMenu(fileName = "Count", menuName = "Collectibles/Count")]
public class CollectibleCount : ScriptableObject
{
    //[HideInInspector]
    public int count;    

    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Awake()
    {
        // Check and count objects of type Collectible
        Type collectibleType = Type.GetType("Collectible");

        if (collectibleType != null)
        {
            count = UnityEngine.Object.FindObjectsByType<Collectible>(FindObjectsSortMode.None).Length;
        }
    }
}

And Collectible script.

using UnityEngine;
using UnityEngine.Audio;

public class Collectible : MonoBehaviour
{
    [SerializeField]
    private float rotationSpeed;

    [SerializeField]
    private GameObject onCollectEffect;

    [SerializeField]
    private GameObject onAllCollectEffect;

    [SerializeField]
    private CollectibleCount total;

    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        transform.Rotate(0, rotationSpeed, 0);      
    }

    public void OnTriggerEnter(Collider other)
    {
        // Destroy a collectible if collides a player

        total.count = total.count - 1;

        if (other.CompareTag("Player") && total.count == 0)
        {
            Instantiate(onAllCollectEffect, transform.position, transform.rotation);

            Destroy(gameObject);
        }
        else if (other.CompareTag("Player"))
        {
            Instantiate(onCollectEffect, transform.position, transform.rotation);

            Destroy(gameObject);
        }
    }
}

Like I already said, scriptable objects do not work like mono behaviours. Awake/Start for scriptable objects do not get called like they would be for a MonoBehaviour. You cannot use a scriptable object this way.

Thank you Mr. Spiney, I fixed the situation with help of my basic knowledge of Model View Controller pattern — in such pattern exist kind of manager-classes, so I created manager class which initialize ScriptableObject instance, I mean its members.

The problem that last — instead of decrement by one point, in collision the count decrements by two point by each collectible… :wheel:

public void OnTriggerEnter(Collider other)
    {
        // Destroy a collectible if collides a player

        total.count = total.count - 1;

I do not know the cause of the problem…

The problem solved, I had few colliders on player-object. For now all works, with an exception — the text updates on each frame. In short, how it works: ScriptableObject initilized by CollectibleManager, the instance of scriptable object during runtime changes by every Collectible, but without events. Then the script connected to Text Mesh GUI object, updates it for every frame.
— The game works correctly. First it plays sound of collect, and on the last collectible it plays winning sound.

What I want to change — maybe avoid update text on the every frame, but for it looks not much expensive. The scripts are below.

using UnityEngine;
using TMPro;
using System; // Required for Type handling

public class UpdateCollectibleCountOne : MonoBehaviour
{
    [SerializeField]
    private TextMeshProUGUI collectibleText; // Reference to the TextMeshProUGUI component

    [SerializeField]
    private CollectibleCount dirtCount;

    void Start()
    {
        
    }

    void Update()
    {
        collectibleText.text = $"Collectibles remaining: {dirtCount.count}";
    }
}

The update script.

using UnityEngine;
using System;

public class CollectibleManager : MonoBehaviour
{
    [SerializeField]
    private CollectibleCount dirtCount;

    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {
        //dirtCount.count = 5;

        // Check and count objects of type Collectible
        Type collectibleType = Type.GetType("Collectible");

        if (collectibleType != null)
        {
            dirtCount.count = UnityEngine.Object.FindObjectsByType<Collectible>(FindObjectsSortMode.None).Length;
        }
    }
}

The manager script.

using UnityEngine;
using UnityEngine.Audio;

public class Collectible : MonoBehaviour
{
    [SerializeField]
    private float rotationSpeed;

    [SerializeField]
    private GameObject onCollectEffect;

    [SerializeField]
    private GameObject onAllCollectEffect;

    [SerializeField]
    private CollectibleCount total;

    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        transform.Rotate(0, rotationSpeed, 0);      
    }

    public void OnTriggerEnter(Collider other)
    {
        // Destroy a collectible if collides a player

        total.count = total.count - 1;

        if (other.CompareTag("Player") && total.count == 0)
        {
            Instantiate(onAllCollectEffect, transform.position, transform.rotation);

            Destroy(gameObject);
        }
        else if (other.CompareTag("Player"))
        {
            Instantiate(onCollectEffect, transform.position, transform.rotation);

            Destroy(gameObject);
        }
    }
}

Collectible script.

After some problems in the real life, I at last back to studying this horrible Unity! :wheel: :slight_smile:
I think the problem is solved, because I managed to use only one update-per-frame logic

using UnityEngine;
using TMPro;
using System; // Required for Type handling

public class UpdateCollectibleCountOne : MonoBehaviour
{
    [SerializeField]
    private TextMeshProUGUI collectibleText; // Reference to the TextMeshProUGUI component

    [SerializeField]
    private CollectibleCount dirtCount;

    // Every-frame method

    void Update()
    {
        collectibleText.text = $"Collectibles remaining: {dirtCount.count}";
    }
}

with help of a so-called Scriptable Object ‘asset’

using UnityEngine;
using System;

[CreateAssetMenu(fileName = "Count", menuName = "Collectibles/Count")]
public class CollectibleCount : ScriptableObject
{
    //[HideInInspector]
    public int count;    
}

and I hope it is better than proposed in tutorial methods and than event-based logic. Again thanks everyone who helped, especially Mr. Piney.

If it works, it works. For something of this scale, many approaches are fine, such as this.

I love events because I only need to add 4 lines of code (event, invoking the event, subscribing to it in another class on enable, unsubscribing from it on disable) and now I can access variables with no references to any instance!