Unity3D: Custom UnityEvent AddListener not firing - unity3d

I have my own custom UnityEvent and am trying to add a listener.
I have used AddListener on numerous other UI objects, such as buttons, dropdowns, toggles, etc. so I understand the process. However, when I Invoke my UnityEvent, it simply doesn't fire.
I'm receiving no error messages, and after doing reading and research, everything looks correct. So, not sure what to do further.
This is an object that emits when it's rotated.
This is the basics of my code:
using UnityEngine.Events;
public class Rotator: MonoBehaviour
{
public UnityEvent OnRotate;
int angle = 0;
int newAngle = 0;
void Start()
{
OnRotate = new UnityEvent();
}
void Update()
{
newAngle = (int)transform.rotation.eulersAngles.z;
if (newAngle != angle)
{
print ("Actual Instance ID: " + GetInstanceID());
print ("Invoking!");
OnRotate.Invoke();
angle = newAngle;
}
}
}
and
public class Owner: MonoBehaviour
{
public Rotator rotator;
void Start()
{
print ("Rotator Instance ID: " + rotator.GetInstanceID());
rotator.OnRotate.AddListener(
() => UpdateRotation()
);
}
void UpdateRotation()
{
print ("Invoked!");
}
}
When the Rotator has it's angle changed, I get this in the console:
Actual Instance ID: 11234
Rotator Instance ID: 11234
Invoking!
The instance ID is to make sure I'm working with the same objects and not going in circles for nothing. They match, so I'm listening to the object that's firing.
However, the listener isn't firing. I've tried different combinations with delegates, etc. but it's all the same. No errors. It just doesn't invoke.
Obviously, I'm doing something wrong, but what is it?
Thanks for any help.

Somehow your answered your new edited version of the question with exactly the code you previously provided in the First Version of your Question!
As I tried to tell you ... if you anywhere in your code do OnRotate = new UnityEvent() of course you thereby erase any persistent callbacks and any runtime callbacks added before that moment!
In short
Simply leave it as
public UnityEvent OnRotate;
and you don't even have to think about it anymore.
For understanding why it also works if you put it in Awake please simply have a look at the Order of Execution for Event Functions
→ First Awake and OnEnabled is called for every GameObject/Component. Then all Start methods are called as soon as the GameObject/Component is active.
Within each of these blocks (Awake + OnEnable) and (Start) the order of execution between different component types is not guaranteed unless you explicitly configure it via the Script Execution Order Settings where you could define that Owner is simply run before Rotator .. then having both in Start would also work again.
Why does it also work if you do it on the public field?
→ Because this field is serialized. That means it is initialized automatically in the Inspector and then stored together with the Scene or prefab asset including any persistent callbacks.
And then Later Unity re-uses the serialized Version of the field so actually you can completely remove the new UnityEvent(); since it doesn't have any effect on a serialized field! It will always be initialized automatically anyway!

Ok, I found out what the issue was.
My question now is "why?".
I changed my code from:
public UnityEvent OnRotate;
void Start() {
OnRotate = new UnityEvent();
}
to
public UnityEvent OnRotate = new UnityEvent();
void Start() {
}
And now it works.
Although, now that I think about it, Awake() is the method where they all fire before initialization, whereas Start() is when the object is created. So the Start() of the Rotator is probably getting called after the Owner is adding a listener.

Related

How to click on instantiated objects and get points, then link points to score text?

What I want:
I want the player to be able to click on instantiated objects and get points, then have those points show in the score-keeping text.
What I’ve done:
I’m currently using the following “FindGameObjectsWithTag” code to retrieve the buttons that are components of the instantiated prefab objects:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
public class CPointScore : MonoBehaviour
{
public TextMeshProUGUI CPointsText;
private float ScoreNum;
private GameObject[] CButtonGmeObjsHolder;
private void CTagFinder()
{
CButtonGmeObjsHolder = GameObject.FindGameObjectsWithTag("Ctag");
foreach (GameObject CButtonGmeObj in CButtonGmeObjsHolder)
{
Debug.Log("GmeObj Found");
Button CButton = CButtonGmeObj.GetComponent<Button>();
CButton.onClick.AddListener(AddScore);
}
}
public void AddScore()
{
ScoreNum += 1;
Debug.Log("Point Added # " + ScoreNum);
}
void Start()
{
InvokeRepeating("CTagFinder", 1f, 15.1f);
}
void Update()
{
CPointsText.text = ScoreNum.ToString();
}
}
Because FindGameObjectsWithTag only calls once I have the InvokeRepeating code in start. I have game objects spawning throughout the duration of the game so it needs to be constantly checking for tags.
Issue:
So the code finds the tags, the buttons are able to be clicked, and the score-keeping text updates which is great. The problem is that if I click one tagged button it will register a point for itself and every tagged button currently in the scene that spawned after it. For example, lets say I have 4 spawned objects currently on scene, when the first object spawned is clicked it will add 4 points instead of 1. If the second object spawned is clicked it will add 3 points instead of 1. I would like to have only the tagged button that is clicked register a point.
Question:
What can I change in my code so that only the tagged button that is clicked registers a point?
Thank you
I think there are two things here:
You repeatedly add the listener so you will end up with multiple callbacks when the button is finally clicked.
The repeated FindGameObjectsWithTag is also quite inefficient
Your main issue is the repeated calling.
For each repeated call of CTagFinder you go through all existing buttons and do
CButton.onClick.AddListener(AddScore);
so these existing buttons end up with multiple listeners attached!
You either want to make sure it is only called once per button, e.g. keeping track of those you already did this for:
private readonly HashSet<Button> alreadyRegisteredButtons = new HashSet<Button>();
and then
if(!alreadyRegisteredbuttons.Contains(CButton))
{
CButton.onClick.AddListener(AddScore);
alreadyRegisteredButtons.Add(CButton);
}
or alternatively make sure you remove the callback before you add it like
CButton.onClick.RemoveListener(AddScore);
CButton.onClick.AddListener(AddScore);
In general I would not use FindGameObjectWithTag an poll objects repeatedly. Rather make your code event driven. This would already avoid the issue at all since there would be no repeated attaching of the listener anyway.
I would simply have a dedicated component YourComponent attached to the same GameObject as the buttons and have a global
public static event Action<YourComponent> OnCTSButtonSpawned;
and in this dedicated component do
private void Start()
{
OnCTSButtonSpawned?.Invoke(this);
}
and in your CPointScore listen to this event like
private void Awake()
{
YourComponent.OnCTSButtonSpawned += AttachListener;
}
private void AttachListener(YourComponent component)
{
if(compoenent.TryGetComponent<Button>(out var button))
{
button.onClick.AddListener(AddScore);
}
}
private void AddScore()
{
ScoreNum++;
CPointsText.text = ScoreNum.ToString();
}

Unity3D New Input System: Is it really so hard to stop UI clickthroughs (or figure out if cursor is over a UI object)?

Even the official documentation has borderline insane recommendations to solve what is probably one of the most common UI/3D interaction issues:
If I click while the cursor is over a UI button, both the button (via the graphics raycaster) and the 3D world (via the physics raycaster) will receive the event.
The official manual:
https://docs.unity3d.com/Packages/com.unity.inputsystem#1.2/manual/UISupport.html#handling-ambiguities-for-pointer-type-input essentially says "how about you design your game so you don't need 3D and UI at the same time?".
I cannot believe this is not a solved problem. But everything I've tried failed. EventSystem.current.currentSelectedGameObject is sticky, not hover. PointerData is protected and thus not accessible (and one guy offered a workaround via deriving your own class from Standalone Input Module to get around that, but that workaround apparently doesn't work anymore). The old IsPointerOverGameObject throws a warning if you query it in the callback and is always true if you query it in Update().
That's all just mental. Please someone tell me there's a simple, obvious solution to this common, trivial problem that I'm just missing. The graphics raycaster certainly stores somewhere if it's over a UI element, right? Please?
I've looked into this a fair bit and in the end, the easiest solution seems to be to do what the manual says and put it in the Update function.
bool pointerOverUI = false;
void Update()
{
pointerOverUI = EventSystem.current.IsPointerOverGameObject();
}
Your frustration is well founded: there are NO examples of making UI work with NewInput which I've found. I can share a more robust version of the Raycaster workaround, from Youtube:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.InputSystem;
using UnityEngine.UI;
/* Danndx 2021 (youtube.com/danndx)
From video: youtu.be/7h1cnGggY2M
thanks - delete me! :) */
public class SCR_UiInteraction : MonoBehaviour
{
public GameObject ui_canvas;
GraphicRaycaster ui_raycaster;
PointerEventData click_data;
List<RaycastResult> click_results;
void Start()
{
ui_raycaster = ui_canvas.GetComponent<GraphicRaycaster>();
click_data = new PointerEventData(EventSystem.current);
click_results = new List<RaycastResult>();
}
void Update()
{
// use isPressed if you wish to ray cast every frame:
//if(Mouse.current.leftButton.isPressed)
// use wasReleasedThisFrame if you wish to ray cast just once per click:
if(Mouse.current.leftButton.wasReleasedThisFrame)
{
GetUiElementsClicked();
}
}
void GetUiElementsClicked()
{
/** Get all the UI elements clicked, using the current mouse position and raycasting. **/
click_data.position = Mouse.current.position.ReadValue();
click_results.Clear();
ui_raycaster.Raycast(click_data, click_results);
foreach(RaycastResult result in click_results)
{
GameObject ui_element = result.gameObject;
Debug.Log(ui_element.name);
}
}
}
So, just drop into my "Menusscript.cs"?
But as a pattern, this is terrible for separating UI concerns. I'm currently rewiring EVERY separately-concerned PointerEventData click I had already working, and my question is, Why? I can't even find how it's supposed to work: to your point there IS no official guide at all around clicking UI, and it does NOT just drop-on-top.
Anyway, I haven't found anything yet which makes new input work easily on UI, and definitely not found how I'm going to sensibly separate Menuclicks from Activityclicks while keeping game & ui assemblies separate.
Good luck to us all.
Unity documentation for this issue with regard to Unity.InputSystem can be found at https://docs.unity3d.com/Packages/com.unity.inputsystem#1.3/manual/UISupport.html#handling-ambiguities-for-pointer-type-input.
IsPointerOverGameObject() can always return true if the extent of your canvas covers the camera's entire field of view.
For clarity, here is the solution which I found worked best (accumulated from several other posts across the web).
Attach this script to your UI Canvas object:
public class CanvasHitDetector : MonoBehaviour {
private GraphicRaycaster _graphicRaycaster;
private void Start()
{
// This instance is needed to compare between UI interactions and
// game interactions with the mouse.
_graphicRaycaster = GetComponent<GraphicRaycaster>();
}
public bool IsPointerOverUI()
{
// Obtain the current mouse position.
var mousePosition = Mouse.current.position.ReadValue();
// Create a pointer event data structure with the current mouse position.
var pointerEventData = new PointerEventData(EventSystem.current);
pointerEventData.position = mousePosition;
// Use the GraphicRaycaster instance to determine how many UI items
// the pointer event hits. If this value is greater-than zero, skip
// further processing.
var results = new List<RaycastResult>();
_graphicRaycaster.Raycast(pointerEventData, results);
return results.Count > 0;
}
}
In class containing the method which is handling the mouse clicks, obtain the reference to the Canvas UI either using GameObject.Find() or a public exposed variable, and call IsPointerOverUI() to filter clicks when over UI.
Reply to #Milad Qasemi's answer
From the docs you have attached in your answer, I have tried the following to check if the user clicked on a UI element or not.
// gets called in the Update method
if(Input.GetMouseButton(0) {
int layerMask = 1 << 5;
// raycast in the UI layer
RaycastHit2D hit = Physics2D.Raycast(Camera.main.ScreenToWorldPoint(Input.mousePosition), Vector2.zero, Mathf.Infinity, layerMask);
// if the ray hit any UI element, return
// don't handle player movement
if (hit.collider) { return; }
Debug.Log("Touched not on UI");
playerController.HandlePlayerMovement(x);
}
The raycast doesn't seem to detect collisions on UI elements. Below is a picture of the Graphics Raycaster component of the Canvas:
Reply to #Lowelltech
Your solution worked for me except that instead of Mouse I used Touchscreen
// Obtain the current touch position.
var pointerPosition = Touchscreen.current.position.ReadValue();
An InputSytem is a way to receive new inputs provided by Unity. You can't use existing scripts there, and you'll run into problems like the original questioner. Answers with code like "if(Input.GetMouseButton(0)" are invalid because they use the old system.

Can't load the In App Purchases when reloading the same scene

I have an issue with my In App Purchases manager. I load my scene and can successfully purchase items. I go to a new scene in game and then back to that scene and now it says...
`MissingReferenceException: The object of type 'IAPManager' has been destroyed but you are still trying to access it.
Your script should either check if it is null or you should not destroy the object.`and then points my to this line
moneyController = GetComponent<MoneyController>();
Why does it crash at that point? Do I need to add DontDestroyOnLoad or something? I'm not familiar with that or how to use it though. Am I missing something simple?
Here are some more code snippets that may or may not prove useful. This code is from a tutorial hence why it's difficult for me to pin point the issue.
public static IAPManager Instance{set;get;}
private void Awake() {
Instance = this;
}
you have to put in DontDestroyOnLoad this way you will not get any error for same. below is the code that you can use.
private static IAPManager instance;
public static IAPManager Instance
{
get
{
if (instance == null)
{
GameObject o = new GameObject ("IAPManager");
instance=o.AddComponent<IAPManager>();
DontDestroyOnLoad (o);
}
return instance;
}
}

Endless Runner Infinite track generation - Unity3D C#

I am new to unity and I am creating my first ever endless runner game. I currently have a cube as a player and another cube (prefab) as the track. I have created a script which aims at instantiating the prefab at runtime and moving the track towards the player (instead of moving the player). For some reason, the tracks instantiated at runtime are being generated at the same position and don't seem to move towards the player. Could anyone tell me if I am doing it right please? How do I generate an infinite/endless track? Thanks in advance
public GameObject[] trackPieces;
public float trackLength;
public float trackSpeed;
private GameObject trackObject;
// Update is called once per frame
void Update ()
{
GenerateInfiniteTrack();
}
private void GenerateInfiniteTrack()
{
trackObject = InstantiatePrefab();
trackObject.transform.Translate(-Vector3.forward * Time.deltaTime * trackSpeed);
}
private GameObject InstantiatePrefab()
{
return Instantiate(trackPieces[Random.Range(0, trackPieces.Length)]) as GameObject;
}
trackObject is overwritten every frame
Your track is not moving because the call to move it occurs only once at instantiation and never again. The value of which track to move is overwritten every update(). Here is what happens:
Every update you make a call to:
private void GenerateInfiniteTrack()
When this method is called you instantiate a new prefab (new instance of your object) and tell the object to move via
trackObject.transform.Translate(-Vector3.forward * Time.deltaTime * trackSpeed);
When you call the method the next time you lose the reference to the original object because you are creating another one that replaces the value held in trackObject. So the previously generated object will never move again. Your new object moves once and then is lost, again, and repeat.
Below I have suggested some code that may work. You will still have to decide how you will dispose of the track objects since you are creating them at a very fast rate. If you can think of a way to use InvokeRepeating() to address this problem, then that would be the cleanest. Invoke repeating has some limitations, like not being able to pass parameters to your repeating function so I did not use it here, but it is conceivably an alternative.
Here is my example.
var TrackObjects = new List<GameObject>();
private void MoveInfiniteTrack()
{
foreach(GameObject gO in TrackObjects )
{
gO.transform.Translate(-Vector3.forward * Time.deltaTime * trackSpeed);
}
}
private void GenerateInfiniteTrack()
{
TrackObject = InstantiatePrefab();
TrackObjects.Add(TrackObject);
}
void Update ()
{
GenerateInfiniteTrack();
MoveInfiniteTrack()
}
void Foo()
{
// Delete your track on some kind schedule or you will quickly get infinite objects and massive lag.
}
As a general observation, creating track this much (60x a second) is very inefficient. I would suggest that you either:
A. Create much larger segments of track, for example once every 1 or 10 seconds and move it along. I don't remember exactly how to do this, but it went something like this:
var lastchecked= datetime.now
Update()
{
if((datetime.now - lastchecked).seconds > 1)
{
// put here what you will do every 1 second.
lastchecked= datetime.now
}
}
B. Create a large cylinder and rotate it to use as a track.

Unity: Weird scene transition bug after adding a "Persistent-Scene" with a GameManager

I made a pretty basic 2D game to learn. I have 2 Scenes, and switching between them worked great. I used empty gameObjects as Start/Exit point of the Scene, so that the game would know to put player on point X after exiting through point X (for example exit outside house if I walk out the door).
Then I added a "Scene0", to use for persistent general scripts like GameManager, Sounds, Music, etc. With just one object called "Controller" that I DontDestroyOnLoad().
After adding this Scene and then just switching Scenes right away to my MainScene, all of a sudden the game starts acting really strange;
the first time I move from my MainScene (Scene1), to my secondary Scene (Scene2), it works fine, but then when I leave Scene2 to go back to Scene1, the player spawns in the middle of nowhere.
And this ONLY happens if I launch the game from my Persistent Scene.
I have no idea what is wrong, I don't add anything that interferes with my scene transitions, all I've added so far is playerHealth, for testing.
Scripts attached to my (persistent) Controller:
DDOL:
public class DDOL : MonoBehaviour {
// Use this for initialization
void Awake () {
DontDestroyOnLoad (gameObject);
}
}
GameManager:
public class GameManager : MonoBehaviour {
public static GameManager manager;
public int playerMaxHealth;
public int playerCurrentHealth;
void Awake(){
if (manager == null) {
manager = this;
} else if (manager != this) {
Destroy (gameObject);
}
}
// Use this for initialization
void Start () {
SceneManager.LoadScene("test_scene");
}
// Update is called once per frame
void Update () {
}
}
Scripts attached to my StartPoint:
PlayerStartPoint:
public class PlayerStartPoint : MonoBehaviour {
private PlayerController thePlayer;
private CameraController theCamera;
public Vector2 startDir;
public string pointName;
// Use this for initialization
void Start () {
thePlayer = FindObjectOfType<PlayerController> ();
if (thePlayer.startPoint == pointName) {
thePlayer.transform.position = transform.position;
thePlayer.lastMove = startDir;
theCamera = FindObjectOfType<CameraController> ();
theCamera.transform.position = new Vector3(transform.position.x, transform.position.y, theCamera.transform.position.z);
}
}
}
And finally ExitPoint:
LoadNewArea:
public class LoadNewArea : MonoBehaviour {
public string levelToLoad;
public string exitPoint;
private PlayerController thePlayer;
// Use this for initialization
void Start () {
thePlayer = FindObjectOfType<PlayerController> ();
}
void OnTriggerEnter2D(Collider2D other){
if (other.gameObject.name == "Player")
{
SceneManager.LoadScene(levelToLoad);
thePlayer.startPoint = exitPoint;
}
}
}
EDIT:
After moving all my DDOL gameObject to the Pre-Scene, it worked. So, I can assume the fault is inside Player or Cameras Start() functions since when they start in Scene1 they get called every time I enter the Scene (only DDOL).
I tried adjusting their Start()functions like follows:
Original camera:
void Start () {
Debug.Log("Starting camera");
if (!cameraExists) {
cameraExists = true;
DontDestroyOnLoad (gameObject);}
else{
Destroy (gameObject);
}
}
Changed Camera:
void Start () {
DontDestroyOnLoad (gameObject);
}
The exact same changes was made in Player.
Obviously this doesnt work because it creates a new Camera/Player every time I enter Scene1 (btw why does it not try to create them when I enter Scene2?, is it because they start in Scene1?). HOWEVER, the new player/camera do start at the correct position, and if I zoom out I can see the old player/camera at that same wrong position as before. So something weird happens when their Start() is called a second time it seems.
You've now mentioned that you had code something like this,
void Start () {
Debug.Log("Starting camera");
if (!cameraExists) {
cameraExists = true;
DontDestroyOnLoad (gameObject);}
else{
Destroy (gameObject);
}
}
Note that this is unfortunately just "utterly incorrect", heh :)
The issues you mention in the question (preload scenes etc) are just totally unrelated to the problem here.
In Unity if you have a character C that persists between scenes a, b, c as you load those scenes, you must kick-off C in it's own (perhaps otherwise empty) scene, you can not use "a" as a matter of convenience to kick off C.
The pattern is, in each of a, b, c just have a line of code like p = FindObjectOfType<Player>(); which runs when the scene loads, and position C as you wish.
Now, regarding your specific puzzle about the unusual behavior you are seeing.
I understand that you want to know why you are observing what you do.
It is a combination of confusion over the following issues: 1 - difference between Awake and Start, 2 - confusion over script execution order {but see below1} 3 - confusion about Destroy versus DestroyImmediate 4 - Not using Debug.Log enough, and not using gameObject.name in there (it's a common in Unity to be wildly confused about which object is talking in Debug.Log) 5 - where you mention you see the other object "off to the side", it's common to drastically confuse which one is which in such situations 6 - confusion between the computer programming concept of "instantiation" (ie, of a class or object) and "instantiating" (confusingly, it's the same word - utterly unrelated) game objects in nity.
If you fiddle around with all those issues, you'll discover an explanation for the behavior you're seeing!
But it doesn't amount to much; in Unity in the "C .. a b c" example you have to create C separately beforehand.
1 {aside, never fiddle with the script execution ordering system in Unity in an effort to solve problems; it's only there for R&D purposes; however it could in fact help you investigate the behavior at hand in this problem, if you are particularly keen to fully understand why you're seeing what you're apparently seeing}
Use the debugger. Have breakpoints at the relevant spots, like PlayerStartPoint.Start() and LoadNewArea.OnTriggerEnter2D() and check that they are executed
At the right time
The right number of times
With the expected values
This should make you see where things get out of hand.
If you use Visual Studio, install https://marketplace.visualstudio.com/items?itemName=SebastienLebreton.VisualStudio2015ToolsforUnity to be able to debug Unity from within Visual Studio.
If you are not using Visual Studio, you probably should.
Is player persistent between scenes (does he have DontDestroyOnLoad)? If no then this might be the reason - you can either try loading the scenes by using the additive mode or by instantiating the player on scene load in correct position.