I'm trying to implement a feature that detects a change in the value of another field object reference.
For example, there is an A object, and the B object contains A.
The A object is notified by OnValidate when the field of the A object has changed.
At this time. Is there any way that "B" can detect the change of "A"?
This feature is only required in the Editor and does not include code in Runtime.
public class A : ScriptableObject
{
[SerializeField]
public string team;
private void OnValidate()
{
Debug.Log($"My team name has been changed to {team}.");
}
}
public class B : ScriptableObject
{
[SerializeField]
public A a;
private void OnValidate()
{
Debug.Log($"A changed.");
}
// How can I detect changes to A and call this functions?
private void OnATeamChanged()
{
Debug.Log($"A's team name has been changed to {a.team}.");
}
private void OnADestroyed()
{
Debug.Log($"A is missing.");
}
}
I would also propose to use events but be careful how and when you register and remove listeners!
public class A : ScriptableObject
{
// You don't need [SerializeField]
// public properties are serialized anyway
public string team;
public event Action onChanged;
public event Action onReset;
public event Action onDestroyed;
private void OnValidate()
{
Debug.Log($"My team name has been changed to {team}.");
if(onChanged == null) return;
onChanged.Invoke();
}
// This is called on reset
// But if you override this you have to set
// The default values yourself!
private void Reset()
{
team = "";
if (onReset == null) return;
onReset.Invoke();
}
// Called when object is destroyed
private void OnDestroyed()
{
if(onDestroyed == null) return;
onDestroyed.Invoke();
}
}
But now in B I would not add the listener on Awake since this adds it after every recompile and multiple times! Instead don't forget to "clean up" all listeners you ever add to avoid getting NullReferenceExceptions:
public class B : ScriptableObject
{
// You don't need [SerializeField] since public properties
// are serialized anyway
public A a;
// Internal for removing listeners later
private A lastA;
private void OnValidate()
{
// Apparently something else was changed
if(a == lastA) return;
Debug.Log("reference a changed");
// Unregister listeners for old A reference
if(lastA)
{
lastA.onChanged -= OnAChanged;
lastA.onReset -= OnAReset;
lastA.onDestroyed -= OnADestroyed;
}
// Register listeners for new A reference
// Note that it is allways possible to remove listeners first
// even if they are not there yet
// that makes sure you listen only once and don't add multiple calls
if(a)
{
a.onChanged -= OnAChanged;
a.onReset -= OnAReset;
a.onDestroyed -= OnADestroyed;
a.onChanged += OnAChanged;
a.onReset += OnAReset;
a.onDestroyed += OnADestroyed;
}
lastA = a;
}
// Make allways sure you also remove all listeners on destroy to not get Null reference exceptions
// Note that it is allways possible to remove listeners even if they are not there yet
private void OnDestroy()
{
if(!a) return;
a.onChanged -= OnAChanged;
a.onReset -= OnAReset;
a.onDestroyed -= OnADestroyed;
}
private void OnAChanged()
{
Debug.Log("A was changed");
}
private void OnAReset()
{
Debug.Log("A was reset");
}
private void OnADestroyed()
{
Debug.Log("a was destroyed");
}
}
Small optional change if you want
If you want additionally to be able to register other listeners in the Inspector the same way you do for Buttons you could simply exchange the
public event Action xy;
with
public UnityEvent xy;
and remove the if(xy == null) checks.
You would than also replace
a.onChanged -= OnAChanged;
a.onChanged += OnAChanged;
by
a.onChanged.RemoveListener(OnAChanged);
a.onChanged.AddListener(OnAChanged);
You could try to add an event
public class A : ScriptableObject
{
[SerializeField]
public string team;
public Action RaiseChangeName;
private void OnValidate()
{
Debug.Log($"My team name has been changed to {team}.");
if(RaiseChangeName != null) { RaiseChangeName(); }
}
}
public class B : ScriptableObject
{
[SerializeField]
public A a;
void Awake()
{
a.RaiseChangeName += OnATeamChanged;
}
// How can I detect changes to A and call this functions?
private void OnATeamChanged()
{
Debug.Log($"A's team name has been changed to {a.team}.");
}
}
Related
I am trying to use virtual and abstract methods to make my game architecture better.
I'm using C# and Unity for this example.
I use a ShipComponent as a base Class because I want all the child classes to do the same thing.
But sometimes I want a certain ShipComponent to do something else.
The code will make it a lot clearer:
ShipComponent.cs:
public abstract class ShipComponent : MonoBehaviour
{
[HideInInspector] public ShipControl shipControl;
public virtual void Init(ShipControl control)
{
this.shipControl = control;
}
public virtual void IsPlayer()
{
SetListeners();
}
public abstract void IsNotPlayer();
public abstract void ReEnable();
public abstract void SetListeners();
}
One of the many child classes that inherits from ShipComponent:
public class Rudder : ShipComponent
{
[Header("Settings")]
public Transform rudder;
[Header("Debug Info")]
[SerializeField] float rudderSpeed;
[SerializeField][Range(-45, 45)] int setRudderAngle = 0;
[SerializeField][Range(-45f, 45f)] float realRudderAngle = 0f;
public override void Init(ShipControl shipControl)
{
base.Init(shipControl);
rudder = transform.GetChild(0).GetChild(4);
StartCoroutine(SmoothRudderChange());
SetListeners();
}
public override void IsPlayer()
{
base.IsPlayer();
}
public override void IsNotPlayer()
{
PlayerShipControl.OnRudderChange -= SetRudder;
}
public override void ReEnable()
{
StartCoroutine(SmoothRudderChange());
SetListeners();
}
public override void SetListeners()
{
PlayerShipControl.OnRudderChange -= SetRudder;
if (!shipControl.shipWrapper.ship.IsPlayer) return;
PlayerShipControl.OnRudderChange += SetRudder;
}
void OnDisable()
{
PlayerShipControl.OnRudderChange -= SetRudder;
StopAllCoroutines();
}
The main draw back I experience with this, is that I have to copy paste all 5 or 6 methods everytime I create a new ShipComponent class.
It seems messy and theres a lot of repeating code, most of the time the only difference in each ShipComponent is the SetListeners part, and StartCoroutines if any.
Is there a way to dynamically set delegate listeners up?
So I could set them in the base class ShipComponent?
Instead of setting each component individually?
Another script that inherits from ShipComponent for completeness:
public class Guns : ShipComponent
{
IEnumerator mouseAimCycle;
public override void Init(ShipControl shipControl)
{
base.Init(shipControl);
InitCannons();
SetListeners();
}
public override void ReEnable()
{
SetListeners();
}
public override void IsPlayer()
{
base.IsPlayer();
mouseAimCycle = AimCycle();
StartCoroutine(mouseAimCycle);
SetListeners();
}
public override void SetListeners()
{
PlayerShipControl.OnFireGuns -= TryFire;
if (!shipControl.shipWrapper.ship.IsPlayer) return;
PlayerShipControl.OnFireGuns += TryFire;
}
public override void IsNotPlayer()
{
StopCoroutine(mouseAimCycle);
PlayerShipControl.OnFireGuns -= TryFire;
}
void OnDisable()
{
PlayerShipControl.OnFireGuns -= TryFire;
StopAllCoroutines();
}
Calling the ShipComponent virtual and abstract methods:
public class ShipControl : MonoBehaviour
{
// Contains Ship + Cargo + Crew and a ref to this ShipControl
public ShipWrapper shipWrapper { get; private set; }
ShipComponent[] shipComponents;
// Gather all ShipComponents and Initialize them.
public void Start()
{
shipComponents = transform.GetComponents<ShipComponent>();
foreach (ShipComponent comp in shipComponents)
{
comp.Init(this);
}
}
// Call this to check if this is players current ship and set the components accordingly.
public void UpdateIsPlayer()
{
if (!shipWrapper.ship.IsPlayer)
foreach (ShipComponent component in shipComponents)
component.IsNotPlayer();
else
foreach (ShipComponent component in shipComponents)
component.IsPlayer();
}
And PlayerShipControl, which I use for input, broadcasting the input through delegates, and the theory is that only the players currently controlled ship will be listening for this input:
public class PlayerShipControl : MonoBehaviour
{
public static event Action<Transform> SetCamToPlayerShip;
public static event Action SetShipPanelUI;
public static event Action<bool> ToggleAnchorIcon, ToggleFlagIcon, ToggleAutofireIcon, ToggleBoatsIcon;
public static event Action OnFireGuns;
public static event Action<int> OnRudderChange;
public static event Action<int> OnSailStateChange;
public static event Action<bool> OnAllAnchorsCommand;
public static event Action<bool> OnAllBoatsCommand;
bool anchor, flag, autofire, boats;
ShipControl shipControl;
void Update()
{
if (Input.GetKeyUp(KeyCode.W)) // Raise Sails SailState++
{
OnSailStateChange?.Invoke(1);
}
if (Input.GetKeyUp(KeyCode.S)) // Furl Sails SailState--
{
OnSailStateChange?.Invoke(-1);
}
if (Input.GetKey(KeyCode.D))
{
OnRudderChange?.Invoke(1);
}
if (Input.GetKey(KeyCode.A))
{
OnRudderChange?.Invoke(-1);
}
if (Input.GetKeyDown(KeyCode.M))
{
OnRudderChange?.Invoke(0);
}
// Drop All Anchors
if (Input.GetKeyDown(KeyCode.V))
{
anchor = true;
ToggleAnchorIcon?.Invoke(anchor);
OnAllAnchorsCommand?.Invoke(anchor);
}
// Haul All Anchors
if (Input.GetKeyDown(KeyCode.H))
{
anchor = false;
ToggleAnchorIcon?.Invoke(anchor);
OnAllAnchorsCommand?.Invoke(anchor);
}
// Drop All Boats
if (Input.GetKeyDown(KeyCode.B))
{
boats = true;
ToggleBoatsIcon?.Invoke(boats);
OnAllBoatsCommand?.Invoke(boats);
}
// Take In All Boats
if (Input.GetKeyDown(KeyCode.U))
{
OnAllBoatsCommand?.Invoke(false);
// TO DO When all boats are back on deck, boatIcon + boatsBoolFlag should be turned off again.
}
if (Input.GetKeyDown(KeyCode.Space))
{
OnFireGuns?.Invoke();
}
}
}
Its a long string of scripts sometimes though so I have left out all the managers and such.
Ship ship inside shipWrapper.ship is a custom data class that stores the info about the ship, not a Monobehaviour, but it holds a bool called IsPlayer aswell. Nothing else of interest I can think of.
The main draw back I experience with this, is that I have to copy paste all 5 or 6 methods every time I create a new ShipComponent class. It seems messy and there's a lot of repeating code, most of the time the only difference in each ShipComponent is the SetListeners part, and StartCoroutines if any.
In the show example you have more differences between implementations then ones described. Without seeing the full code it is hard to suggest something meaningful.
Few notes on the current code:
In Rudder you don't need to specify IsPlayer because the following:
public override void IsPlayer()
{
base.IsPlayer();
}
does not add anything extra, so you can just skip implementation in the derived class.
Based on provided examples it seems that ReEnable can be defined as virtual in base class with default implementation set to calling SetListeners (the same approach as you have with Init and IsPlayer).
PlayerShipControl.Update possibly can be improved by moving handlers to dictionary. Something along this lines:
public class PlayerShipControl : MonoBehaviour
{
// ...
Dictionary<KeyCode, Action> keyActions = new() // not sure about the type
{
{ KeyCode.W, () => OnSailStateChange?.Invoke(1) },
// ...
{ KeyCode.V, () =>
{
anchor = true;
ToggleAnchorIcon?.Invoke(anchor);
OnAllAnchorsCommand?.Invoke(anchor);
}
},
// ...
};
void Update()
{
foreach (var kvp in keyActions)
{
if (Input.GetKeyUp(kvp.Key))
{
kvp.Value();
break;
}
}
}
}
This question already has answers here:
array of events in C#?
(4 answers)
Closed 1 year ago.
I'm creating a touch manager, which fires Began and Ended events and other monobehaviours can subscribe to them. For some reason I am unable to find any information on creating a list of events actions.
This is an illustration of what I currently have:
public class TouchHandler : MonoBehaviour {
private static readonly int MAX_TOUCH_COUNT = 2;
public event Action<int> Began;
public event Action<int> Ended;
private void Update() {
if (Input.touchCount > 0) {
Touch[] touches = Input.touches;
int touchCount = Mathf.Min(touches.Length, MAX_TOUCH_COUNT);
for (int i = 0; i < touchCount; i++) {
Touch t = touches[i];
if (t.phase == TouchPhase.Began)
Began?.Invoke(i);
else if (t.phase == TouchPhase.Ended)
Ended?.Invoke(i);
}
}
}
}
public class Listener : MonoBehaviour {
[SerializeField] private TouchHandler th;
private int touchIndexToListen = 0;
private void OnEnable() {
th.Began += TouchBegan;
th.Ended += TouchEnded;
}
private void OnDisable() {
th.Began -= TouchBegan;
th.Ended -= TouchEnded;
}
private void TouchBegan(int index) {
if (index != touchIndexToListen)
return;
Debug.Log("TouchBegan");
}
private void TouchEnded(int index) {
if (index != touchIndexToListen)
return;
Debug.Log("TouchEnded");
}
}
This is how I'd want it to work:
public class TouchHandler : MonoBehaviour {
private static readonly int MAX_TOUCH_COUNT = 2;
public event Action[] Began = new Action[MAX_TOUCH_COUNT];
public event Action[] Ended = new Action[MAX_TOUCH_COUNT];
private void Update() {
if (Input.touchCount > 0) {
Touch[] touches = Input.touches;
int touchCount = Mathf.Min(touches.Length, MAX_TOUCH_COUNT);
for (int i = 0; i < touchCount; i++) {
Touch t = touches[i];
if (t.phase == TouchPhase.Began)
Began[i]?.Invoke();
else if (t.phase == TouchPhase.Ended)
Ended[i]?.Invoke();
}
}
}
}
public class Listener : MonoBehaviour {
[SerializeField] private TouchHandler th;
private int touchIndexToListen = 0;
private void OnEnable() {
th.Began[touchIndexToListen] += TouchBegan;
th.Ended[touchIndexToListen] += TouchEnded;
}
private void OnDisable() {
th.Began[touchIndexToListen] -= TouchBegan;
th.Ended[touchIndexToListen] -= TouchEnded;
}
private void TouchBegan(int index) {
Debug.Log("TouchBegan");
}
private void TouchEnded(int index) {
Debug.Log("TouchEnded");
}
}
So in short: I want to subscribe only to specific touch indexes, which also helps me get rid of the if (index != touchIndexToListen) check in other monobehaviours.
The reason I'm doing this touch manager is because I want a clean way to handle mouse input using the same class and I need to be able to fire the ended event when app is minimized in Android. I've left those parts out of this illustration, since they are irrelevant to this question.
First of all one important note: The i used for touches[i] is NOT guaranteed to refer to the same Touch across multiple frames!
For that you rather want to use the Touch.fingerId
The unique index for the touch.
All current touches are reported in the Input.touches array or by using the Input.GetTouch function with the equivalent array index. However, the array index is not guaranteed to be the same from one frame to the next. The fingerId value, however, consistently refers to the same touch across frames. This ID value is very useful when analysing gestures and is more reliable than identifying fingers by their proximity to previous position, etc.
Touch.fingerId is not the same as "first" touch, "second" touch and so on. It is merely a unique id per gesture. You cannot make any assumptions about fingerId and the number of fingers actually on screen, since virtual touches will be introduced to handle the fact that the touch structure is constant for an entire frame (while in reality the number of touches obviously might not be true, e.g. if multiple tappings occur within a single frame).
The array makes no sense actually. event can only be used for a single delegate (Action is just a shorthand for a delegate void Action();) it can't be used for an Action[].
You actually can store a single Action for each index and then use the += and -= operators on it.
You could go a complete different way and use a pattern like
public class TouchHandler : MonoBehaviour
{
private const int MAX_TOUCH_COUNT = 2;
// Store callback Actions by phase and index
// NOTE: I would still pass in the actual Touch in order to be able to use the information in it
private readonly Dictionary<TouchPhase, Dictionary<int, Action<Touch>>> _touchActions = new Dictionary<TouchPhase, Dictionary<int, Action<Touch>>>
{
{TouchPhase.Began, new Dictionary<int, Action<Touch>>()},
{TouchPhase.Ended, new Dictionary<int, Action<Touch>>()}
};
// I would keep one where the listeners can still check "manually"
public event Action<Touch> generalTouchEvent;
public void Subscribe(TouchPhase phase, int touchIndex, Action<Touch> action)
{
if (touchIndex < 0 || touchIndex > MAX_TOUCH_COUNT)
{
Debug.LogError($"Touch index {touchIndex} is not supported!", this);
return;
}
// First get the according inner dictionary by TouchPhase
if (!_touchActions.TryGetValue(phase, out var touchPhaseActions))
{
Debug.LogError($"Touch phase {phase} is not supported!", this);
return;
}
// Next check if there already is an action for given index
if (touchPhaseActions.TryGetValue(touchIndex, out var touchAction))
{
// If it already exists we append the new action to the existing one
// Unsubscribing is fine even if it wasn't added before
// This just makes sure it is only added once
touchAction -= action;
touchAction += action;
}
else
{
// otherwise this new action is the only one for now
touchAction = action;
}
// (over)write it back into the dictionary
touchPhaseActions[touchIndex] = touchAction;
}
public void UnSubscripe(TouchPhase phase, int touchIndex, Action<Touch> action)
{
if (touchIndex < 0 || touchIndex > MAX_TOUCH_COUNT)
{
Debug.LogError($"Touch index {touchIndex} is not supported!", this);
return;
}
// First get the according inner dictionary by TouchPhase
if (!_touchActions.TryGetValue(phase, out var touchPhaseActions))
{
Debug.LogError($"Touch phase {phase} is not supported!", this);
return;
}
if (touchPhaseActions.TryGetValue(touchIndex, out var touchAction))
{
touchAction -= action;
touchPhaseActions[touchIndex] = touchAction;
}
else
{
Debug.LogWarning($"Nothing was listening to {phase} on index {touchIndex} anyway");
}
}
private void Update()
{
if (Input.touchCount > 0)
{
// Get all touches, order them ascending by their fingerId then take up to MAX_TOUCH_COUNT of them
foreach (var t in Input.touches.OrderBy(t => t.fingerId).Take(MAX_TOUCH_COUNT))
{
InvokeTouchEvent(t);
}
}
}
private void InvokeTouchEvent(Touch touch)
{
generalTouchEvent?.Invoke(touch);
if (!_touchActions.TryGetValue(touch.phase, out var touchPhaseActions))
{
return;
}
if (!touchPhaseActions.TryGetValue(touch.fingerId, out var touchAction))
{
return;
}
touchAction?.Invoke(touch);
}
}
and then on the listener do e.g.
public class TouchListener : MonoBehaviour
{
[SerializeField] private TouchHandler _touchHandler;
[SerializeField] public int touchIndexToListenTo;
private void Start()
{
_touchHandler.Subscribe(TouchPhase.Began, touchIndexToListenTo, OnTouchBegan);
_touchHandler.Subscribe(TouchPhase.Ended, touchIndexToListenTo, OnTouchEnded);
_touchHandler.generalTouchEvent += OnGeneralTouchEvent;
}
private void OnGeneralTouchEvent(Touch touch)
{
Debug.Log($"Received a general callback for {touch.phase} with index {touch.fingerId}!", this);
}
private void OnTouchEnded(Touch touch)
{
Debug.Assert(touch.fingerId == touchIndexToListenTo, $"Why do I get an event for {touch.fingerId} if I'm only listening to {touchIndexToListenTo}?!", this);
Debug.Assert(touch.phase == TouchPhase.Ended, $"Why do I get an event for {touch.phase} if I'm only listening to {TouchPhase.Ended}?!", this);
Debug.Log($"Received a {touch.phase} event for index {touch.fingerId}", this);
}
private void OnTouchBegan(Touch touch)
{
Debug.Assert(touch.fingerId == touchIndexToListenTo, $"Why do I get an event for {touch.fingerId} if I'm only listening to {touchIndexToListenTo}?!", this);
Debug.Assert(touch.phase == TouchPhase.Began, $"Why do I get an event for {touch.phase} if I'm only listening to {TouchPhase.Began}?!", this);
Debug.Log($"Received a {touch.phase} event for index {touch.fingerId}", this);
}
}
You can use EventHandlerList for subscribing multiple kind of event in same object.
Be aware that keys are references, so passing int there and retrieving it by value will not work. You must save object references somewhere to be able to fire specific events.
Simple purpose demo app here:
class Program
{
static void Main(string[] args)
{
EventHandlerList _eventList = new EventHandlerList();
var mouseHandler = new MouseHandlerClass();
var appStateHandler = new AppStateHandlerClass();
//attach mouse handlers
_eventList.AddHandler(mouseHandler, (Action<int>)mouseHandler.DoSomething);
_eventList.AddHandler(mouseHandler, (Action<int>)mouseHandler.DoAnotherThing);
//attach appState handlers
_eventList.AddHandler(appStateHandler, (Action<int>)appStateHandler.DoSomething);
//fire mouse event
(_eventList[mouseHandler] as Action<int>).Invoke(99);
//fire appState event
(_eventList[appStateHandler] as Action<int>).Invoke(52);
}
}
public class MouseHandlerClass
{
public void DoSomething(int input)
{
Console.WriteLine($"MouseHandler processed: {input}");
}
public void DoAnotherThing(int input)
{
Console.WriteLine($"MouseHandler processed another thing: {input}");
}
}
public class AppStateHandlerClass
{
public void DoSomething(int input)
{
Console.WriteLine($"AppStateHandlerClass processed: {input}");
}
}
Edit
The handler method signature can be simplified to Action if you don't need to pass any parameters to attached handlers
In short. I have a tunnel building game where every tunnel piece has a Vector2 ChunkedPostion that generally describes where they are located in world space.
The problem is I need to Recalculate some things when one of these tunnels is destroyed and it would take longer than necessary to reload the whole map. Therefore, I'm hoping someone could tell me how to pass along this Chunkedpostion in an event that's called on the destruction of a tunnel so each tunnel could quickly check if the event is relevant to them before making the calculations.
simplified code:
public class transmitter : MonoBehaviour
{
public delegate void SendSignel();
public static event SendSignel OnSendSignel;
Vector2 ChunkedPostion;
private void Start()
{
ChunkedPostion = new Vector2(transform.position.x, transform.position.z);
}
void Update()
{
if (Input.GetKeyDown("s"))
{
OnSendSignel();//<= i would like to pass along ChunkedPostion in the Event
}
}
}
public class receiver : MonoBehaviour
{
//and then have someway to "Debug.Log" the recieved coordinates
private void OnEnable() { transmitter.OnSendSignel += PrintSignel; }
private void OnDisable() { transmitter.OnSendSignel -= PrintSignel; }
void PrintSignel()
{
Debug.Log("received");
}
}
Working Code:
public class transmitter : MonoBehaviour
{
//public delegate void SendSignel();
public delegate void SendSignel(Vector2 SenderPostion);
public static event SendSignel OnSendSignel;
Vector2 ChunkedPostion;
private void Start()
{
ChunkedPostion = new Vector2(transform.position.x, transform.position.z);
}
void Update()
{
if (Input.GetKeyDown("s"))
{
OnSendSignel(ChunkedPostion);//<= i would like to pass along ChunkedPostion in the Event
}
}
}
public class receiver : MonoBehaviour
{
//and then have someway to "Debug.Log" the recieved coordinates
private void OnEnable() { transmitter.OnSendSignel += PrintSignel; }
private void OnDisable() { transmitter.OnSendSignel -= PrintSignel; }
//void PrintSignel()
//{
// Debug.Log("received");
//}
void PrintSignel(Vector2 SenderPostion)
{
Debug.Log("received");
Debug.Log("at: " + SenderPostion);
}
}
I'm pretty sure you're using the wrong syntax, at least in the question you are.
public static event SendSignel OnSendSignel;
//should be
public static event EventHandler<Vector2> OnSendSignal
using this you'll be able to provide OnSendSignal a vector2 when you invoke it
I'm confused about finding reference GameObject but different Scene and set the onclick when difference scene, so I have GameManager who manage all but available only on the Main menu. So I decide to make Dontdestroyonload, the issue start at this, so when I play to the MainGame Scene, the field of GameManager at inspector will find, but I can't drag n drop different scene, right? That confuses me.
And if the GameManager at the MainMenu scene, the question is how to drag n drop at the onClick event, like I want pause button active or something else in the game.
]3
I tried with onLloadscene(scene s, Mode mode), but nothing happens, and here the scrip for the GameManager. :
public static GameManager gameManager;
[Header("Main Menu panels")]
public GameObject startPanel;
public GameObject settingPanel;
public GameObject levelPanel;
[Header("InGame Panels")]
#region Panel
public GameObject pausePanel;
public GameObject ObjectivePanel;
public GameObject shopPanel;
private int click = 0;
[Header("Int Tweaks")]
public int indexLevel;
public int onlevel;
public bool isPaused;
_levelSelect LevelSelect;
public static GameManager Instance { set; get; }
public int levelindexPlayerPrefs;
private void Awake()
{
if (gameManager != null)
{
Instance = this;
Destroy(gameObject);
}
else
{
DontDestroyOnLoad(gameObject);
}
}
void Start()
{
LevelSelect = FindObjectOfType<_levelSelect>();
OnStart();
onlevel = int.Parse(LevelSelect.levelIndex) + 1;
indexLevel = int.Parse(LevelSelect.levelIndex);
getPlayerData();
}
// Update is called once per frame
void Update()
{
ExitApp();
}
public void OnStart()
{
startPanel.SetActive(true);
settingPanel.SetActive(false);
levelPanel.SetActive(false);
}
#region Buttons
public void startbutton()
{
levelPanel.SetActive(true);
startPanel.SetActive(false);
settingPanel.SetActive(false);
}
public void backButtonMainMenu()
{
levelPanel.SetActive(false);
startPanel.SetActive(true);
settingPanel.SetActive(false);
}
public void settingbutton()
{
levelPanel.SetActive(false);
startPanel.SetActive(false);
settingPanel.SetActive(true);
}
public void PauseButton()
{
Time.timeScale = 0f;
pausePanel.SetActive(true);
ObjectivePanel.SetActive(false);
}
public void Resume()
{
Time.timeScale = 1f;
}
#endregion
public void ExitApp()
{
if (Input.GetKey(KeyCode.Escape))
{
click++;
StartCoroutine(ClickTime());
if (click>1)
{
print("Exit Game");
Application.Quit();
}
}
}
IEnumerator ClickTime()
{
yield return new WaitForSeconds(0.5f);
click = 0;
}
public void getPlayerData()
{
levelindexPlayerPrefs = PlayerPrefs.GetInt("LevelIndex", 0);
}
public void updateLevel(int Index)
{
if (levelindexPlayerPrefs < Index)
{
PlayerPrefs.SetInt("LevelIndex", Index);
levelindexPlayerPrefs = PlayerPrefs.GetInt("LevelIndex");
}
}
#region onloadedScenePickRefferences
private void OnEnable()
{
SceneManager.sceneLoaded += OnSceneLoaded;
}
private void OnDisable()
{
SceneManager.sceneLoaded -= OnSceneLoaded;
}
void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
pausePanel = GameObject.FindGameObjectWithTag("PausePanel");
ObjectivePanel = GameObject.FindGameObjectWithTag("ObjectivePanel");
}
#endregion
//public IEnumerator EndChapter()
//{
// updateLevel(indexLevel + 1);
// getPlayerData();
//}
Here is what I would probably do:
Have a static class for storing and sharing all your references. It doesn't have to be in any scene but simply "lives" in the assets:
public static class GlobalReferences
{
// as example just for one reference but you can implement the rest equally yourself
// here this class actually stores the reference
private static GameObject startPanel;
// A public property in order to add some logic
// other classes will always access and set the value through this property
public static GameObject StartPanel
{
get
{
// if the reference exists return it right away
if(startPanel) return startPanel;
// as a fallback try to find it
startPanel = GameObject.FindGameObjectWithTag("StartPanel");
// ofcourse it might still fail when you simply try to access it
// in a moment it doesn't exist yet
return startPanel;
}
set
{
startPanel = value;
// invoke an event to tell all listeners that the startPanel
// was just assigned
OnStartPanelReady?.Invoke();
}
}
// An event you will invoke after assigning a value making sure that
// other scripts only access this value after it has been set
// you can even directly pass the reference in
public static event Action<GameObject> OnStartPanelReady;
}
So now in your component(s) that is(are) in the new loaded scene you assign the value as early as possible (Awake). Here you can already store it via the Inspector since it is a scene reference:
public class ExampleSetter : MonoBehaviour
{
// already reference it via the Inspector
[SerializeField] private GameObject startPanel;
private void Awake()
{
// as a fallback
if(!startPanel) startPanel = GameObject.FindObjectWithTag("startPanel");
// assign it to the global class
GlobalReferences.StartPanel = startPanel;
}
}
And in other scenes that where already loaded before you add a listener so they do their stuff as soon as the other scene is ready:
public class ExampleConsumer : MonoBehaviour
{
[Header("Debug")]
[SerializeField] private GameObject startPanel;
private void Awake()
{
// Try to get the reference
startPanel = GlobalReferences.StartPanel;
// if this failed then wait until it is ready
if(!startPanel)
{
// it is save to remove callbacks even if not added yet
// makes sure a listener is always only added once
GlobalReferences.OnStartPanelReady -= OnStartPanelReady;
GlobalReferences.OnStartPanelReady += OnStartPanelReady;
}
// otherwise already do what you want
else
{
OnStartPanelReady(startPanel);
}
}
private void OnDestroy()
{
// always make sure to clean up callbacks when not needed anymore!
GlobalReferences.OnStartPanelReady -= OnStartPanelReady;
}
private void OnStartPanelReady(GameObject newStartPanel)
{
startPanel = newStartPanel;
// always make sure to clean up callbacks when not needed anymore!
GlobalReferences.OnStartPanelReady -= OnStartPanelReady;
// NOTE: It is possible that at this point it is null anyway if another
// class has set this actively to null ;)
if(startPanel)
{
// Now do something with the startPanel
}
}
}
The other way round when you need a reference in the new loaded Scene form the main scene ... it should already be set since the mainscene was loaded first and has already assigned its according references.
Now you can either go for this static class or simply implement the same logic for each reference that needs to be shared directly in an according component where you reference them via drag&drop .. it makes no difference since anyway you will use static fields and events that are not bound to any instance but the type(s) itself.
I'm new to events and delegates and I'm having trouble making the functional connection between things, specifically the events manager and how events get connected to specific buttons in my program. So far I've created a few delegates and then I have a class that should subscribe to those delegates but I don't get how this system actually gets connected to buttons on different scenes. I just need someone to help me make the connection so that I can see how everything functions. Thank you.
Here is my event manager class
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Event_Manager : MonoBehaviour
{
public static Event_Manager evt = null; // create singleton
public delegate void GoToStartingSceneDelegate();
public static GoToStartingSceneDelegate onGoToStartingSceneDelegate;
public delegate void GoToSelectionSceneDelegate();
public static GoToSelectionSceneDelegate onGoToSelectionSceneDelegate;
public delegate void GoToColoringSceneDelegate();
public static GoToColoringSceneDelegate onGoToColoringSceneDelegate;
public delegate void GoToCaveSceneDelegate();
public static GoToCaveSceneDelegate onGoToCaveSceneDelegate;
private void Awake()
{
if (evt == null)
evt = this;
else if (evt != null)
Destroy(gameObject);
DontDestroyOnLoad(gameObject);
}
public static void OnStartGameButtonClick()
{
Debug.Log("Start Game");
if (onGoToStartingSceneDelegate != null)
onGoToStartingSceneDelegate();
}
public static void OnStartOverButtonClick()
{
Debug.Log("Start Over");
if (onGoToSelectionSceneDelegate != null)
onGoToSelectionSceneDelegate();
}
public static void OnSendAnimalButtonClick()
{
Debug.Log("Send Animal");
if (onGoToCaveSceneDelegate != null)
onGoToCaveSceneDelegate();
}
public static void OnPlayAgainButtonClick()
{
Debug.Log("Play Again");
if (onGoToStartingSceneDelegate != null)
onGoToStartingSceneDelegate();
}
}
and this is the class that I'm wanting to subscribe to those events
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
using System.Collections;
using UnityEngine.UI.ProceduralImage;
public class LoadSceneButtonController : MonoBehaviour
{
[SerializeField] public Object startingScene;
[SerializeField] public Object selectionScene;
[SerializeField] public Object coloringScene;
[SerializeField] public Object caveScene;
GameObject transitionManager;
public GameObject touchToPlayButton;
public GameObject sendAnimalToForestButton;
public GameObject startOverButton;
private void OnEnable()
{
Event_Manager.onGoToStartingSceneDelegate += GoToStartingScene;
Event_Manager.onGoToSelectionSceneDelegate += GoToSelectionScene;
Event_Manager.onGoToColoringSceneDelegate += GoToColoringScene;
Event_Manager.onGoToCaveSceneDelegate += GoToCaveScene;
}
private void OnDisable()
{
Event_Manager.onGoToStartingSceneDelegate -= GoToStartingScene;
Event_Manager.onGoToSelectionSceneDelegate -= GoToSelectionScene;
Event_Manager.onGoToColoringSceneDelegate -= GoToColoringScene;
Event_Manager.onGoToCaveSceneDelegate -= GoToCaveScene;
}
void GoToStartingScene()
{
SceneManager.LoadScene(startingScene.name.ToString());
}
void GoToSelectionScene()
{
SceneManager.LoadScene(selectionScene.name.ToString());
}
void GoToColoringScene()
{
SceneManager.LoadScene(coloringScene.name.ToString());
}
void GoToCaveScene()
{
SceneManager.LoadScene(caveScene.name.ToString());
}
Your code is not working but the problem has nothing to do with your events or delegates.
The problem is how you declared your scene variables. Do not use UnityEngine.Object for that. If you do, you can only cast it to SceneAsset which is from the UnityEditor namespace. This means that your code will only work in the Editor with the AssetDatabase class.
Use UnityEngine.SceneManagement instead of UnityEngine.Object. You are currently passing Object.name to the SceneManager.LoadScene function and Object.name refers to the name of the Object not a scene name.
Replace all:
[SerializeField] public Object startingScene;
[SerializeField] public Object selectionScene;
[SerializeField] public Object coloringScene;
[SerializeField] public Object caveScene;
with
public Scene startingScene;
public Scene selectionScene;
public Scene coloringScene;
public Scene caveScene;
Here is a simple Unity tutorial for events and delegates. I don't think you need that since that part of your code looks fine.
As for connecting it to your buttons, it is very easy. Just subscribe to the onClick.AddListener button event and notify the Event_Manager event when there is a click.
public Button startingButton;
public Button selectionButton;
public Button coloringButton;
public Button CaveButton;
void OnEnable()
{
//Register Button Events
startingButton.onClick.AddListener(() => buttonCallBack(startingButton));
selectionButton.onClick.AddListener(() => buttonCallBack(selectionButton));
coloringButton.onClick.AddListener(() => buttonCallBack(coloringButton));
CaveButton.onClick.AddListener(() => buttonCallBack(CaveButton));
}
private void buttonCallBack(Button buttonPressed)
{
if (buttonPressed == startingButton)
{
Debug.Log("Clicked: " + startingButton.name);
Event_Manager.OnStartGameButtonClick();
}
if (buttonPressed == selectionButton)
{
Debug.Log("Clicked: " + selectionButton.name);
Event_Manager.OnStartOverButtonClick();
}
if (buttonPressed == coloringButton)
{
Debug.Log("Clicked: " + coloringButton.name);
Event_Manager.OnSendAnimalButtonClick();
}
if (buttonPressed == CaveButton)
{
Debug.Log("Clicked: " + CaveButton.name);
Event_Manager.OnSendAnimalButtonClick();
}
}