3

I know how to access another script's variable, but I can do only via function calls or delegates. The first one is easy, but makes the code fragile becuase once I edit the original, I have to edit again. The second one is better but as I have a lot of functions, with different kinds of return values and parameters, it would complicate things a lot. Say I want to do some stuff at the beginning of the game. So far I created a function named OnGameStart() in the appropriate script and called everything I will need from there, and that OnGameStart() was made public and was called from another script.

I will need to play sounds, check save data, play UI and other animations and so on at the beginning but I don't want to make my code a disaster. I looked for this online but found only the simplest "how to communicate between scripts" stuff, which goes with the basic function call, sometimes events. Could someone skilled guide me to resources on how to make compact, segregated classes that hold up Demeter's law?

agiro
  • 2,018
  • 2
  • 30
  • 62

2 Answers2

2

There are definitely a lot of possibilities to address such problem, you could for instance take some inspiration from the Hollywood principle.

Instead of your Player searching for something, provide it to him at initialization.

Here's a really quick example:

Definition of a game manager interface:

using UnityEngine;

namespace Assets.Scripts
{
    public interface IGameManager
    {
        void PlayAudioClip(AudioClip audioClip);
    }
}

Definition of a game manager:

using UnityEngine;

namespace Assets.Scripts
{
    public class GameManager : MonoBehaviour, IGameManager
    {
        #region IGameManager Members

        public void PlayAudioClip(AudioClip audioClip)
        {
            // TODO
        }

        #endregion
    }
}

An example:

using System;
using UnityEngine;

namespace Assets.Scripts
{
    public class Player : MonoBehaviour
    {
        public GameManager GameManager; // TODO assign this in Inspector

        public void Start()
        {
            if (GameManager == null)
                throw new InvalidOperationException("TODO");
        }

        public void Update()
        {
            // demo
            var wounded = true;
            var woundedAudioClip = new AudioClip();
            if (wounded)
            {
                GameManager.PlayAudioClip(woundedAudioClip);
            }
        }
    }
}

You could also have a used a sort of Singleton in Unity, (or whatever appropriate).

Notes:

The example above is really just an example to give a you hint on how to think, you haven't provided all the details and even if you did, we could hardly help you any further (only you will find over time what's really appropriate to your current problem).

When advancing in your game project you will see that there are no hard rules to follow, obviously patterns are important but you will probably find yourself ending up with your own (i.e. a finely-grained combined usage of multiple patterns).

Maybe 'Who wrote this programing saying? "Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live."' will give you some inspiration too.

Conclusion is, discover/try patterns, adapt them, and over time you will find what's working the best for you.

Community
  • 1
  • 1
aybe
  • 15,516
  • 9
  • 57
  • 105
  • This is an example of the _"everything must be a MonoBehaviour"_ Unity anti-pattern. It tends to be more error prone than using a static instance (and you're treating it like it's static anyway, only with much more overhead, more inspector effort and awkward cases as you have to try to serialize _everything_ which is often entirely unnecessary). – Luke Briggs Feb 07 '17 at 21:14
  • It definitely outweighs the usage of `static`, read on about the benefits of using the Composition pattern VS using `static` (if there can be any sort of comparison between both). In your example you put `Player.Current` but if you lean back a bit, you'll sooner or later realize it doesn't make sense at all :) – aybe Feb 07 '17 at 21:30
  • It's not a one or the other thing; use both at the same time - I've been involved a few hundred Unity projects so I'm _rather certain_ that's how most Unity developers work :) For example, I'd imagine you've written a script which needs to access the player GameObject which hasn't been instanced yet. That'll result in a line like `GameObject.Find(...)` which is both slow and error prone. Either store a static reference to that gameObject, or at the very least, wrap it in some static property. – Luke Briggs Feb 07 '17 at 21:39
  • Thank you all for your answers. Indeed currently I use a `Singleton` as a `MainGameManager` and handle most of the calls from there. For non-functional logic I have a `UImanager` for example so I have the like 2 scripts where things actually happen. I read both answers and comments thoroughly, will most certainly pick up something. – agiro Feb 08 '17 at 09:21
  • @agiro FWIW, avoid making a class called `GameManager` - such a name is practically destined to become a god class :) I.e. what does it need to manage that some more appropriately named class can't do instead (such as a `BackgroundMusic` class that deals with playing background music, rather than a PlayMusic method showing up in this "manager" class). – Luke Briggs Feb 08 '17 at 13:37
  • Heh you got it right, @LukeBriggs, my `GameManager` is indeed a god class, with that sole intention though. But it's a god only of the `functional`, or **`High priority`** part of the game, the UI anims, saves and so on have lower priority and are written with `coroutines` - expected to finish on time only **more or less**, and having **no `FixedUpdate()` or `Update()`** function in them. – agiro Feb 08 '17 at 14:24
1

Use static classes

Static classes are great for whenever you need broad access to some subsystem of your program, and they're widely used in games as a result.

/// <summary>Save/load is a good example because it 
/// doesn't need any settings and it's 
/// useful to call it from almost anywhere.</summary>
public static class GameSaver {

    /// <summary>I save the game when I'm run.</summary>
    public static void Save() {
       // Save the game!
    }

}

To use a static class, you just use the member directly - for example GameSaver.Save(); will work from "anywhere". Properties, fields, events etc can all be made static, but see the notes below first.

This is the easiest way of avoiding some kind of "god class" - that appears to be what you're describing (and yes, they are often a code disaster!) - that's a class which is excessively complicated and does everything. Break it up into a series of small, self contained modules.

Don't overuse static fields though!

Use a singleton for that.

Particularly in gaming, it's really common to have things that are only instanced once (like, say, the player or the audio system) which also needs to be easy to reset or has a large number of properties.

It's important to not have them all as static fields - that would be difficult to reset and hard to debug. That's where you'd use a static field and instance an ordinary class - this is called a singleton:

/// <summary>There's only ever one background music source!
/// It has instance properties though (i.e. an AudioSource)
/// so it works well as a singleton.</summary>
public class BackgroundMusic {

    /// <summary>The static field - use the Play method from anywhere.</summary>
    private static BackgroundMusic Current;

    /// <summary>Plays the given clip.</summary>
    public static void Play(AudioClip clip) {

        if (Current == null) {
            // It's not been setup yet - create it now:
            Current = new BackgroundMusic();
        }

        // E.g. Current.Source.Play(clip);

    }

    public BackgroundMusic() {
        // Instance a source now. 
    }

}

This just means BackgroundMusic.Play(..); is available from anywhere. This kind of approach means that you don't need to setup anything in the inspector - just calling that method sets itself up on demand.

When MonoBehaviour is great

It's common to think that all code must be a MonoBehaviour and must be attached to a gameobject. That's not how Unity actually works; that just results in more work for whoever is using the editor when everything is a MonoBehaviour and must all be manually instanced and connected up.

To be clear, I'm not saying don't use MonoBehaviour at all. Rather, you should use an appropriate combination of the component model and static depending on what the code actually represents.

In general:

  • If there is only one instance of something, use a singleton.
  • But if there's only one and it has properties that are useful to edit in the inspector, use a MonoBehaviour and keep a reference to the single object as a static field too.

An example of that would be the player (in a single player game) with a range of default settings that you'd like to vary. You would setup the player as a prefab and have some kind of PlayerSettings.Current static field which references the current instance:

/// <summary>Add this to a player prefab.</summary>
public class PlayerSettings : MonoBehaviour{

    /// <summary>Still following the singleton pattern.</summary>
    public static PlayerSettings Current;

    /// <summary>Player speed. This can be edited in the inspector.</summary>
    public float Speed;


    public void Awake() {
        // Update the static field:
        Current = this;
    }

}

This kind of approach gives a best of both worlds - you can use PlayerSettings.Current from anywhere (after the player prefab has been instanced) without having to give up the inspector. It's also much easier to refactor than something like GameObject.Find("Player/Body").GetComponent<PlayerSettings>(); too, making it easier to maintain.

Multiple instances

If there are multiple instances of something, like NPC's, then generally you'd always use prefabs with MonoBehaviour's there. However, using static methods can be extremely useful with those:

public class NPC : MonoBehaviour{

    /// <summary>Gets an NPC by their name.</summary>
    public static NPC Locate(string name){
        // E.g. get all GameObject instances with an NPC component.
        // Return the first one which has a 'Name' value that matches.
    }

    /// <summary>The name of this NPC (editable in the inspector).
    public string Name;

}

NPC.Locate("Dave"); becomes fairly self-explanatory as to what it's actually expected to do.

Luke Briggs
  • 3,745
  • 1
  • 14
  • 26
  • Beware of `static` in Unity, it's evil :) – aybe Feb 07 '17 at 21:03
  • @Aybe not anymore - many years ago when the editor didn't unload static fields properly it was a nightmare, but those days are long since gone. – Luke Briggs Feb 07 '17 at 21:04
  • To my knowledge, it's still evil as of today :) (https://docs.unity3d.com/Manual/script-Serialization.html) – aybe Feb 07 '17 at 21:08
  • @Aybe That's something very different - that's referring to static fields in a MonoBehaviour, which are not expected to be serialized anyway. I.e. their initial value (if any) is correctly stored in the DLL as is the case with any other C# program. – Luke Briggs Feb 07 '17 at 21:12
  • You know about the subtleties but maybe the OP does not (an explanation of what's happening under the hood and the possible caveats would be welcome). – aybe Feb 07 '17 at 21:21
  • @Aybe Serialization is something very different though; it's how Unity stores values defined in the inspector. In your answer for example, there aren't any values being serialized anyway - `PlayAudioClip` is much better off as a static method :) – Luke Briggs Feb 07 '17 at 21:28
  • Let me clarify, it is the usage of `static` keyword in this specific context that IMO is wrong, having `static` methods is another thing which obviously has its benefits. – aybe Feb 07 '17 at 21:33
  • static purpose is not to make communication between scripts easy. GetComponent is. static is meant to create a member to belong to the class as opposed to the instance. You can create a singleton pattern via static as well. But your definition is plain wrong. It is working in a limited way. How do you use your example for many enemies, projectiles, buildings? – Everts Feb 07 '17 at 21:58
  • @Everts this isn't saying only use static classes; in _those_ cases you would obviously use instances. This is answering, roughly, _"my game has a lot of functions that I'd like to access from many places - how should I do it?"_ – Luke Briggs Feb 07 '17 at 22:04
  • @Aybe I've made this answer clearer to better describe the combination of both. – Luke Briggs Feb 07 '17 at 22:45
  • 1
    Great, I agree with your overall approach, it's just that the lack of explanations was confusing and led to a potentially endless debating/flaming war :) – aybe Feb 07 '17 at 23:01
  • Looking at the latest addition...well...then how do you get an enemy called "Enemy (Clone)" over another enemy called "Enemy (Clone)"? Or do you actually name all the enemies with a specific name? I'm thinking the airport scene in call of duty, imagine naming all those people with specific names...considering also some people have same names...maybe you should use their SSN to be sure. – Everts Feb 08 '17 at 07:29
  • @Everts Good one, once I ran into this naming stuff, ever since I use `Find-by-name` methods only for unique objects. I usually `tag` them and keep tracking them in a `List` or any other `Generic collection`. Unless there are many. In that case I would make only "dumb" AI-s and try to control them as a "mass" using a manager. I have never had to implement such feature yet though. – agiro Feb 08 '17 at 09:44
  • @Everts a method like that is, of course, for unique NPCs only. E.g. The player talks to a uniquely identifiable NPC. Crowds are clearly not the intended purpose. Static _properties_ can be most useful for those, such as `Zombies.All` (which internally does a find by tag, for example). It's far easier to maintain - e.g. maybe you originally used `All{ get{ return GameObject.FindGameObjectsWithTag("zombie"); } }` and later decided to store some `List` of them. Just changing that one definition is much more appropriate. – Luke Briggs Feb 08 '17 at 13:29