Search Unity

How to get a scene to ignore a reference

Discussion in 'Scripting' started by DustyShinigami, Jul 21, 2019.

  1. DustyShinigami

    DustyShinigami

    Joined:
    Jan 5, 2018
    Posts:
    529
    At the moment I have my Screen Fader set up so when the screen fades to black, the player GameObject is deactivated, and when it un-fades, the player is re-activated. This is to stop the player from being moved whilst the screen fades in and out between a new scene. The player is in each scene, so it can be found ...except in two of them... Those would be my Main Menu and Credits. However, with how I've set it up, those scenes can't find the player, so gives me the 'Object reference is not set to an instance of an object' error.

    What could I do to prevent the Screen Fader from trying to find the player in those two scenes?

    This is the ScreenFader script I have:

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEngine.UI;
    5. using UnityEngine.SceneManagement;
    6.  
    7. public class ScreenFader : MonoBehaviour
    8. {
    9.     public Image blackScreen;
    10.     public float fadeSpeed;
    11.     public float waitForFade;
    12.     public static bool black;
    13.     public GameObject thePlayer
    14.     {
    15.         get
    16.         {
    17.             if (_thePlayer != null)
    18.                 return _thePlayer;
    19.  
    20.             _thePlayer = GameObject.Find("Player");
    21.             return _thePlayer;
    22.         }
    23.     }
    24.  
    25.     private GameObject _thePlayer = null;
    26.     private bool isFadeToBlack;
    27.     private bool isFadeFromBlack;
    28.  
    29.     void Start()
    30.     {
    31.         thePlayer.SetActive(false);
    32.         StartCoroutine("ScreenFade");
    33.     }
    34.  
    35.     IEnumerator ScreenFade()
    36.     {
    37.         if (SceneManager.GetActiveScene() == SceneManager.GetSceneByName("level 1, room 1"))
    38.         {
    39.             isFadeToBlack = false;
    40.         }
    41.         else if (black)
    42.         {
    43.             yield return new WaitForSeconds(fadeSpeed);
    44.             isFadeToBlack = true;
    45.             yield return new WaitForSeconds(waitForFade);
    46.             isFadeFromBlack = true;
    47.             yield return new WaitForSeconds(0.1f);
    48.             thePlayer.SetActive(true);
    49.         }
    50.         else
    51.         {
    52.             yield return new WaitForSeconds(fadeSpeed);
    53.             isFadeToBlack = true;
    54.             yield return new WaitForSeconds(waitForFade);
    55.             isFadeFromBlack = true;
    56.             yield return new WaitForSeconds(0.1f);
    57.             thePlayer.SetActive(true);
    58.         }
    59.     }
    60.  
    61.     void Update()
    62.     {
    63.         if (isFadeToBlack)
    64.         {
    65.             blackScreen.color = new Color(blackScreen.color.r, blackScreen.color.g, blackScreen.color.b, Mathf.MoveTowards(blackScreen.color.a, 1f, fadeSpeed * Time.deltaTime));
    66.             if (blackScreen.color.a == 1f)
    67.             {
    68.                 isFadeToBlack = false;
    69.             }
    70.         }
    71.         if (isFadeFromBlack)
    72.         {
    73.             blackScreen.color = new Color(blackScreen.color.r, blackScreen.color.g, blackScreen.color.b, Mathf.MoveTowards(blackScreen.color.a, 0f, fadeSpeed * Time.deltaTime));
    74.             if (blackScreen.color.a == 0f)
    75.             {
    76.                 isFadeFromBlack = false;
    77.             }
    78.         }
    79.         if (black)
    80.         {
    81.             blackScreen.color = new Color(blackScreen.color.r, blackScreen.color.g, blackScreen.color.b);
    82.             if (blackScreen.color.a == 1f)
    83.             {
    84.                 black = false;
    85.             }
    86.         }
    87.     }
    88. }
    89.  
     
  2. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    Multiple solutions to that

    Pretty dirty: Add a dummy object which can be found. This is similar to something called the null object pattern, which is sometimes used in programming. That is, the absence of an object is not "null", but an object that can be addressed but doesn't do anything useful.
    In your case, that dummy object would take the place of such an empty implementation.
    Downside: If you turn your player reference into a component, you'd need to provide a component which at least satisfies the interface you need.

    Also dirty: Multiple implementations, for instance, via inheritance... I won't spend any time getting into why this is not worth the effort...

    Quick and halfway dirty (if you need to fix that ASAP):
    Add a boolean to the script that indicates whether the player is required or not. If it is, access the player object, if it isn't, let your code run into exceptions (indicates a bug), or log your own errors / throw your own exceptions.

    Similar to the above (quick fix):
    Always do the null-checl before you access it. This is probably the most appropriate, but it is certainly different to tell from code whether that scene's screen fader should find a player.

    Downside: You might forget the player object in a scene, or you may simply forget to name it correctly or to name it at all. Something might rename it. Or you change the structure or add multiple characters for a player...

    Those are all valid but dirty and actually really bad solutions, they cannot satisfy your needs in the long run, if you asked me.

    If I had to choose one of those above solutions, I'd take the latter but would exchange the Find call with an inspector field.

    A different perspective...

    If I had the choice to improve that now when it's still not a complex setup, I'd completely decouple the screen fader from the player. After all, you've already ran into the situations in which the player does not exist in some of the scenes that need a screen fader.

    In order to improve that for more flexibility and long term maintainance, you'll need remove the responsibility from the screen fader.
    Simplest approach is to introduce a component that controls that specific aspect of the application. Give that very specific component the references for all that stuff that needs to complete before controls of the player and other stuff will be enabled.
    You can do that by event subscriptions or polling, no matter what you choose ... the screen fader does it's own thing it's supposed to do, and it lets the "outer world" (i.e. the rest of your application) know when it's done... that "outer world" then decides how things go on.
    You'll see how easy it'll be to extend that, as opposed to the solution you're currently stuck with. If another component needs to complete its work as well, you'll have a conflict and need to re-do everything. Because that screen fader would set the character controllable, whereas another component would've needed the opposite.
    In contrast, the controller would just wait for another component to be ready, and until then, keep the player controls disabled.
     
  3. DustyShinigami

    DustyShinigami

    Joined:
    Jan 5, 2018
    Posts:
    529
    Thanks for the lengthy response. So, what you're saying in the end, is to not have my Screen Fader control whether or not the player is active...? To just let it do its own thing, which is to control whether the screen fades to black or not? As for the component that should control specific aspects like that, I'm not sure how I'd do that. I'm not familiar with event subscriptions or polling. :-\
     
  4. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    Exactly. It's a small component which you can use to construct a more complex system. It shouldn't even know in which context this is used.

    The other component is just another script which references your fader, and (for now) the player, as an example.
    Events are not complicated at all, just look them up. As an alternative, you can start with either Update or a coroutine and check a boolean value once per frame, which is the indicator for being ready.
     
  5. DustyShinigami

    DustyShinigami

    Joined:
    Jan 5, 2018
    Posts:
    529
    I've only looked into Events briefly, so I still don't quite understand them, how they work, or how to implement them properly, but I'll continue to research.

    I have thought of a possible alternative solution though. Would pausing the in-game timer work fine in this case? Like with the pause menu, the game is paused until resumed. Maybe whilst the screen fader is doing its thing, the game timer could be paused to prevent the player from being able to move, and then it's un-paused once the screen fader has finished...? Unless pausing it prevents the screen fader from working too...
     
  6. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    That's another approach, but the fader needs to depend on the unscaled time if you do that.

    They are usually very intuitive once you get a hang on them. It's just a different way to communicate between objects.

    I recommend to try out some examples, and experiment with it a little bit.

    It certainly makes sense to have a direct comparison of

    1) direct dependency / your version (screen fader knows the exact instances it needs to operate on, in this case a gameobject)

    2) some sort of observer pattern (screen fader also knows the type of the objects it needs to notify, but usually limited to a small interface that the observers implement)

    3) events (screen fader raises an event, which indicates the fade in / fade out is complete), doesn't need to know any of the specific type of the ones who subscribe to it

    4) polling (screen fader exposes properties that you can frequently check in Update or in a coroutine), so it doesn't need to know anything either

    Perhaps I can post some examples later.
     
  7. DustyShinigami

    DustyShinigami

    Joined:
    Jan 5, 2018
    Posts:
    529
    Okay, I have a bit of an understanding of how they work and how to set them up, however in this case, I'm struggling to figure out how I'd incorporate it. :-\

    I also tried adjusting the in-game timer and it's not a viable option; everything pauses, which stops the fader from working.
     
  8. Joe-Censored

    Joe-Censored

    Joined:
    Mar 26, 2013
    Posts:
    11,847
    I'd just disable input and movement of the character instead of the entire object. Worst case I'd disable all the object's components, and re-enable when faded back in.

    If I didn't like those, I'd have another gameobject with DontDestroyOnLoad set, which just contains a reference to the player gameobject. When loaded in the new scene you just get the reference from there.
     
  9. DustyShinigami

    DustyShinigami

    Joined:
    Jan 5, 2018
    Posts:
    529
    That's what I did initially - disable input/movement of the character, but I could never get it to last long enough before the screen fader finished. Even with a coroutine and WaitForSeconds, the movement would be disabled, but it would re-enable before the fader finished. Just couldn't get it right.

    I'm still a bit confused with setting up references. I have done it countless times, but I'm still not 100% sure/confident on the best procedure to do it. I'd prefer doing it via code instead of having to keep dropping an object into the Inspector, especially if a new scene has all of its own objects and it can't find the reference from the previous scene.
     
  10. DustyShinigami

    DustyShinigami

    Joined:
    Jan 5, 2018
    Posts:
    529
    Okay, after a bit of procrastinating, as well as working on other projects, I've looked a bit more into Delegates and Events and have a bit more of an understanding of them and how they work. However, I'm having issues getting the player to reactivate. I've noticed that if I put 'if' statements in an invoked method, the event doesn't get called. I take it they don't work for invoked methods for an event...? Secondly, if I want the player deactivated and then reactivated, I will need to set up two separate delegates and events...? Lastly, I'm guessing you can't invoke two separate methods within the same block of code, or in a coroutine...? I'm thinking that's why I can't get the player to reactivate...

    PlayerController:

    Code (CSharp):
    1. void OnEnable()
    2.     {
    3.         ScreenFader.deactivatePlayer += DisablePlayer;
    4.         ScreenFader.reactivatePlayer += EnablePlayer;
    5.     }
    6.  
    7.     void OnDisable()
    8.     {
    9.         ScreenFader.deactivatePlayer -= DisablePlayer;
    10.         ScreenFader.reactivatePlayer -= EnablePlayer;
    11.     }
    12.  
    13. void DisablePlayer()
    14.     {
    15.         gameObject.SetActive(false);
    16.     }
    17.  
    18.     void EnablePlayer()
    19.     {
    20.         gameObject.SetActive(true);
    21.     }
    ScreenFader:

    Code (CSharp):
    1. public delegate void PlayerDeactivated();
    2. public static event PlayerDeactivated deactivatePlayer;
    3. public delegate void PlayerReactivated();
    4. public static event PlayerReactivated reactivatePlayer;
    5.  
    6. IEnumerator ScreenFade()
    7.     {
    8.         if (SceneManager.GetActiveScene() == SceneManager.GetSceneByName("level 1, room 1"))
    9.         {
    10.             isFadeToBlack = false;
    11.         }
    12.         else if (black)
    13.         {
    14.             Invoke("DeactivatePlayer", 0.5f);
    15.             yield return new WaitForSeconds(fadeSpeed);
    16.             isFadeToBlack = true;
    17.             Invoke("ReactivatePlayer", 0.5f);
    18.             yield return new WaitForSeconds(waitForFade);
    19.             isFadeFromBlack = true;
    20.         }
    21.         else
    22.         {
    23.             Invoke("DeactivatePlayer", 0.5f);
    24.             yield return new WaitForSeconds(fadeSpeed);
    25.             isFadeToBlack = true;
    26.             Invoke("ReactivatePlayer", 0.5f);
    27.             print("reactivated player");
    28.             yield return new WaitForSeconds(waitForFade);
    29.             isFadeFromBlack = true;
    30.         }
    31.     }
    32.  
    33.     void DeactivatePlayer()
    34.     {
    35.         deactivatePlayer();
    36.     }
    37.  
    38.     void ReactivatePlayer()
    39.     {
    40.         reactivatePlayer();
    41.     }
     
  11. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    Sorry for not posting back earlier. I tend to forget about threads when I say something like I'll try to add example later.

    You should debug the code and check what is being executed. Make sure the subscriptions are done, and that they're done early enough.

    Also, four things that immediately stand out are these:
    1) Your events are static.

    I'd try to avoid that, unless you absolutely have to. Generally, statics are often misused.
    There's a lot to say about it, but that'd be a long post.

    2) I know you're new to events. And this is probably just a first approach and not so important. However, the names of your events should be changed, because the names kind of suggests what should happen / what should be done when they're raised, instead of telling any arbitrary subscriber what the instance itself is doing done, or what the instance itself has done.

    3) events should always be null-checked before invocation. Reason being, that there can be zero subscribers. Depending on the language version, you have more or less convenient ways to do that. There are also workarounds, but IMO it's cleaner to just null-check, as that's also the standard.

    4) I recommend to use a wait instruction in your coroutine rather than "Invoke" with a delay.


    Unfortunately I don't have the time to code up some examples for all the various approaches I mentioned in a previous post.
     
  12. DustyShinigami

    DustyShinigami

    Joined:
    Jan 5, 2018
    Posts:
    529
    I did do some, and the event was/is being called when the player is deactivated, but not the reactivate event, which is why I asked earlier if multiple invokes can be used in the same block of code. It doesn't appear as though the second one is.

    Oh. All the tutorials I've watched seem to have the events static so they can be called/accessed in other scripts.

    I don't understand this one; I thought they could be called anything like a variable or function...?

    I did do this initially, but the code changed to a darkened grey colour in Visual Studio and the tooltip says 'Delegate invocation can be simplified'. And that's just from using

    Code (CSharp):
    1. if(deactivatePlayer != null)
     
    Last edited: Jul 29, 2019
  13. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    Consider this:

    OnEnable is only called when
    - component comes alive AND the component starts enabled
    - component was disabled, and it is re-enabled
    - component itself is enabled, but its GameObject was inactive and is now activated

    If you think about it, this could be your bug:

    Your player GameObject and the component come alive => subscribes to the events.
    Screen fader raises the event which indicates the action is complete, player unsubscribes.

    Now, you're sitting there with an inactive player GameObject, and a player component that has no active subscription, since we unsubscribed when it was disabled.

    Screen fader starts fading to black, but the player is not subscribed, because it always unsubscribes when it is disabled.

    For a quick test, try to subscribe in the player components Awake/Start and unsubscribe in OnDestroy.


    Unfortunately, many tutorials tend to use shortcuts and uncommon or even improper coding techniques for the sake of brevity and simplicity, because they focus on demonstrating the general concept of a specific feature.

    Yes, they can have arbitrary names.
    However, you usually don't want to express what the subscribers should do. Instead, you want to express what the event sender is doing/has done.

    So it's just about the semantics.
    I mean, check your event names.
    Intuitively, you've chosen imperative names. They read like instructions, like "do that", "process that".
    But most of the time, you want to notify subscribers about your own actions or state, "im starting", "I'm done".


    Suppose you also want to play a sound when the screen fades out.

    Do you subscribe to "DeactivePlayer", or does your screen fader suddenly declare a second event "PlaySound"?
    Both is not desired. It couples the screen fader semantically to things it should probably not know anything about.

    Yes, as I stated earlier, it depends on which version of the language you're compatible with.

    The recommended way to raise an event used to be lengthy
    Code (csharp):
    1. var handler = SomethingHappened; // thread-safety measure in multi-threaded environments
    2. // from now on, use the copy
    3. if (handler != null)
    4. {
    5.     handler(...);
    6.     // or
    7.     handler.Invoke(...);
    8. }
    Since the introduction of the null-propagation operator, this overly lengthy event raising pattern can be shortened:
    Code (csharp):
    1. SomethingHappened?.Invoke(...);
    Note the ? operator. You're missing that one in your examples. :)
     
    DustyShinigami likes this.
  14. DustyShinigami

    DustyShinigami

    Joined:
    Jan 5, 2018
    Posts:
    529
    You were absolutely right! Having the player unsubscribe in OnDestroy allowed him to disable and then re-enable. Thank you. :D So, if the player GameObject is disabled at any point, it can't access the events and they're immediately disabled...?

    If I'm honest, I don't really see how it matters regarding their names, but I'm still a newbie to all of this. :p You might have to show me some examples. All the ones I've seen have used arbitrary names and I know of no other way when it comes to variable and function names.

    I've only come across the '?' operator once. In this case, I don't really know how to use it properly. I tried, but it gave me an error. :-\
     
  15. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    No, they can always react to the events, unless you unsubscribe.

    Before you moved that to OnDestroy, your player unsubscribed when it was disabled, so it could not receive the reactivation event.


    Names don't matter for the technical part. It works the same with all the names you that are valid

    However, we already decoupled the fader from the other parts of your game, so that's why you should also change the semantics of that event in order to make it absolutely self-contained.

    Compare yours: "reactivePlayer" with "FadeOutCompleted"

    The former does still semantically know something it doesn't need to care about. It should not know what's going to happen when it faded in or out (you know it, but the fader should not). It also does not express clearly when it's going to be called, leaving subscribers in the dark of whether that's what they're looking for.

    The latter eliminates both disadvantages: it doesn't assume there's something like a player, so it still keeps the fader self-contained (semantically) and doesn't take it beyond its own problem domain. It also mentions when it is raised.

    In addition to that, you can also parametrize events so that they can pass some useful data to the subscribers.

    Perhaps you're not working with the most recent API compatibility level. You can either check and fix that, or use the long version I posted earlier.
    Like I mentioned, there's also a trick which allows to omit the null-checking, which can be accomplished by always having at least one empty subscription. But that's up to the conventions you're using, or at least it's up to your personal preference.
     
  16. DustyShinigami

    DustyShinigami

    Joined:
    Jan 5, 2018
    Posts:
    529
    I'm still a bit confused with this. How exactly did it unsubscribe when it was disabled? Was that due to it being null...? I don't think I put anything about what it should do if it is null, only when it's not equal to null. o_O

    So... is naming them correctly just for the purposes of making the code tidier...? For anyone else reading the code to see that the Screen Fader is its own thing and that it's not referring to something it can't possibly know about...?

    I'm not sure how I'd do that. :confused: