Your issue is going to be the order of events. Before you made your change, a call was being made to your delegate before the subscribers had subscribed. Whilst all of the Start methods are called before say, Update, you cannot guarantee that they will be executed in your preferred order unless you implement the Script Execution Order and effectively weight them. Where-ever possible I would avoid using the Script Execution Order as it provides a fantastic way of hiding what is going on.
You could also consider the OnEnable and OnDisable methods for use with delegates, as really you should be unsubscribing from them too.
Because the Awake method is called before Start, this is why after you made your change your code doesn’t error, as all of the subscribers are no in place before a call is made to the delegate.
Okay! I unstained what you meant now, sorry. I must have missed the “!” before the = sign.
I tried fixing the problem for 2 hours but to no avail…
I am still getting null references at:
if (Input.GetMouseButton (0))
{
notifyMouseClickObservers(priorityHit.Value, layerHit);
}
When I used Debug.Log for the outputs of priorityHit.value and layerHit within the != null statement, I never got any logs in the console. So I Only have null.
My PlayerMovement script is as follows:
using System;
using UnityEngine;
using UnityStandardAssets.Characters.ThirdPerson;
using UnityEngine.AI;
[RequireComponent(typeof (NavMeshAgent))]
[RequireComponent(typeof (AICharacterControl))]
[RequireComponent(typeof (ThirdPersonCharacter))]
public class PlayerMovement : MonoBehaviour {
AICharacterControl aiCharacterControl = null;
ThirdPersonCharacter thirdPersonCharacter = null;
CameraRaycaster cameraRaycaster = null;
Vector3 currentDestination;
private bool isInDirectMode = false;
void Start()
{
cameraRaycaster = Camera.main.GetComponent<CameraRaycaster>();
thirdPersonCharacter = GetComponent<ThirdPersonCharacter>();
currentDestination = transform.position;
aiCharacterControl = GetComponent<AICharacterControl>();
cameraRaycaster.notifyMouseClickObservers += ProcessMouseClick;
}
void ProcessMouseClick(RaycastHit raycastHit, int layerHit)
{
print("Click");
}
//TODO Make this get called again
void ProcessDirectMovement()
{
Debug.Log("In direct movement mode");
float h = Input.GetAxis("Horizontal");
float v = Input.GetAxis("Vertical");
// calculate camera relative direction to move:
// calculate move direction to pass to character
Vector3 cameraForward = Vector3.Scale(Camera.main.transform.forward, new Vector3(1, 0, 1)).normalized;
Vector3 movement = v * cameraForward + h * Camera.main.transform.right;
thirdPersonCharacter.Move(movement, false, false);
}
}
Should I post my CameraRaycaster or CursorAffordance script as well?
I have the code checked for if it is null.
(Sorry for the bad formatting, I will correct that)
Code for my ProcessMovement:
[RequireComponent(typeof (NavMeshAgent))]
[RequireComponent(typeof (AICharacterControl))]
[RequireComponent(typeof (ThirdPersonCharacter))]
public class PlayerMovement : MonoBehaviour {
AICharacterControl aiCharacterControl = null;
ThirdPersonCharacter thirdPersonCharacter = null; // A reference to the ThirdPersonCharacter on the object
CameraRaycaster cameraRaycaster = null;
Vector3 currentDestination; // required so we don't start moving in some arbitray direction or start moving, until we actually click.
private bool isInDirectMode = false; //TODO Consider making static later if other scripts
//require this information.
void Awake()
{
cameraRaycaster = Camera.main.GetComponent<CameraRaycaster>();
cameraRaycaster.notifyMouseClickObservers += ProcessMouseClick;
}
void Start()
{
thirdPersonCharacter = GetComponent<ThirdPersonCharacter>();
currentDestination = transform.position;
aiCharacterControl = GetComponent<AICharacterControl>();
}
void ProcessMouseClick(RaycastHit raycastHit, int layerHit)
{
print("Click");
}
//TODO Make this get called again
void ProcessDirectMovement()
{
Debug.Log("In direct movement mode");
float h = Input.GetAxis("Horizontal");
float v = Input.GetAxis("Vertical");
// calculate camera relative direction to move:
// calculate move direction to pass to character
Vector3 cameraForward = Vector3.Scale(Camera.main.transform.forward, new Vector3(1, 0, 1)).normalized;
Vector3 movement = v * cameraForward + h * Camera.main.transform.right;
thirdPersonCharacter.Move(movement, false, false);
}
}
The CameraRaycaster script is as follows:
public class CameraRaycaster : MonoBehaviour
{
// INSPECTOR PROPERTIES RENDERED BY CUSTOM EDITOR SCRIPT
[SerializeField] int[] layerPriorities; // int value based, compared to OLD with layer types
float maxRaycastDepth = 100f; // Hard coded value
int topPriorityLayerLastFrame = -1;
public delegate void OnCursorLayerChange(int newLayer); // declare new delegate type
public event OnCursorLayerChange notifyLayerChangeObservers; // instantiate an observer set
public delegate void OnClickPriorityLayer(RaycastHit raycastHit, int layerHit); // declare new delegate type
public event OnClickPriorityLayer notifyMouseClickObservers; // instantiate an observer set
void Update()
{
// Check if pointer is over an interactable UI element
if (EventSystem.current.IsPointerOverGameObject ())
{
NotifyObserersIfLayerChanged (5);
return; // Stop looking for other objects
}
// Raycast to max depth and store in array named 'raycastHits', every frame as things can move under mouse
Ray ray = Camera.main.ScreenPointToRay (Input.mousePosition);
RaycastHit[] raycastHits = Physics.RaycastAll (ray, maxRaycastDepth);
RaycastHit? priorityHit = FindTopPriorityHit(raycastHits);
if (!priorityHit.HasValue) // if hit no priority object
{
NotifyObserersIfLayerChanged (0); // broadcast default layer
return;
}
// Notify delegates of layer change
var layerHit = priorityHit.Value.collider.gameObject.layer; // Too many Dots!
NotifyObserersIfLayerChanged(layerHit);
// Notify delegates of highest priority game object under mouse when clicked
if (Input.GetMouseButton (0))
{
if (notifyMouseClickObservers != null)
{
Debug.Log(priorityHit.Value);
Debug.Log(layerHit);
notifyMouseClickObservers(priorityHit.Value, layerHit);
}
}
}
void NotifyObserersIfLayerChanged(int newLayer)
{
if (newLayer != topPriorityLayerLastFrame)
{
topPriorityLayerLastFrame = newLayer;
notifyLayerChangeObservers (newLayer);
}
}
RaycastHit? FindTopPriorityHit (RaycastHit[] raycastHits)
{
// Form list of layer numbers hit
List<int> layersOfHitColliders = new List<int> ();
foreach (RaycastHit hit in raycastHits)
{
layersOfHitColliders.Add (hit.collider.gameObject.layer);
}
// Step through layers in order of priority looking for a gameobject with that layer
foreach (int layer in layerPriorities)
{
foreach (RaycastHit hit in raycastHits)
{
if (hit.collider.gameObject.layer == layer)
{
return hit; // stop looking
}
}
}
return null;
}
}
CursorAffordance Script:
[RequireComponent (typeof(CameraRaycaster))]
public class CursorAffordance : MonoBehaviour {
[SerializeField] Texture2D walkCursor = null;
[SerializeField] Texture2D targetCursor = null;
[SerializeField] Texture2D unknownCursor = null;
[SerializeField] Vector2 cursorHotspot = new Vector2(0 , 0); // The actual clicking point on the cursor
//TODO solve fight between serialize and const
[SerializeField] const int walkableLayerNumber = 9;
[SerializeField] const int enemyLayerNumber = 10;
[SerializeField] const int deadLayerNumber = 11;
CameraRaycaster cameraRaycaster;
void Start() {
cameraRaycaster = GetComponent<CameraRaycaster>();
cameraRaycaster.notifyLayerChangeObservers += OnLayerChangeForCursor; // Registering to oberver list.
}
// Only called when layer changes.
void OnLayerChangeForCursor(int newLayer) {
switch (newLayer) // Used to say "switch (cameraRaycaster.currentLayerHit)" before we passed parameters with delegates.
{
case walkableLayerNumber:
Cursor.SetCursor(walkCursor, cursorHotspot, CursorMode.Auto);
break;
case enemyLayerNumber:
Cursor.SetCursor(targetCursor, cursorHotspot, CursorMode.Auto);
break;
default:
//Debug.LogError("Don't know what cursor to show");
Cursor.SetCursor(unknownCursor, cursorHotspot, CursorMode.Auto);
return;
}
}
}
Can you confirm which line the error is on, in the above it states line 81, but that would be a closing brace at present, so it clearly isn’t that.
Note, if you don’t post full scripts then the line numbers don’t match up when other people check your code, case in point, all of the missing using directives etc.
If it’s easier, share the project with me and I’ll take a look. The forum will allow uploads of up to 10mb, your project is likely to be larger than that you’ll need to use a service such as Google Drive or Dropbox. Please zip the project files and then share the URL and I’ll take a look for you.
I waited as long as I could but had to get some sleep in the end, grabbing a copy of your project now, will take a look
Updated Wed Sep 05 2018 13:53
Just open up the project, I wasn’t sure which scene specifically you were running, so I started with Combat Sandbox, no errors. I placed the mouse over an enemy and it changed to a sword icon, I moved the mouse over the grass, it changed to an arrow - seems ok.
I then tried the Level 1 Village scene, and did encounter an error, in fact, I saw three errors;
Starting from the top, the first error is caused because this line of code;
is trying to act upon members of a variable, cameraRaycaster, which itself is null. The reason cameraRaycaster is null is due to the previous line in the Awake statement;
Here, you are using GetComponent and specifying the type as CameraRaycaster, but if you look at your Main Camera you’ll note that you don’t have a CameraRaycaster component attach to it, what you do have is a CameraRaycaster_Old component attached. It looks like you have probably renamed it but not changed this line above, and as such the variable is null because no component was found/returned.
The second error message is due to the exact same problem, again, no CameraRaycaster component could be found/was returned, due to the renaming.
And finally, the third error is caused because the delegate has no subscribers, because of the renaming, and the cameraRaycaster variable being null in the other two scripts, nothing actually ever subscribed.
So, to start to resolve this problem I would suggest you either remove the component flagged old if it shouldn’t be attached anymore, and then attach the correct one - or - rename this one accordingly so that your code can find the correct component.
After this, let me know where things are
Updated Wed Sep 05 2018 14:47
If you add the CameraRaycaster.cs script component, and remove the CameraRaycaster_OLD.cs script component and then run the game you’ll be presented with the following error;
If we look at the code in question;
if (EventSystem.current.IsPointerOverGameObject ())
{
NotifyObserersIfLayerChanged (5);
return; // Stop looking for other objects
}
Line 54 is the first line, you are trying to acces the EventSystem, if you check your scene you’ll note that you don’t have one of these objects in the Hierarchy.
After adding one, and running the scene again, no errors are presented, instead your Debug.Log messages appear in the console;
The next issue is that you can’t really test the cursor affordance because of the position of your player in the scene, so, if you move your player, for now, to somewhere like 5, -100, 210, and then drag an enemy prefab into the scene, somewhere near to the player and run the game, you can now move your mouse over all of the layers you are interested in, the ground, the enemy and the unknown grey world space…
(video best played in full screen mode for clarity)
Okay! Sorry for the long wait! My college classes started ahahah
I’m looking at everything now, and Wow… Thank you. To be honest, I think I got all mixed up with version control at some point. I must have had trouble, reverted to an older build thinking my scripts had been added at that point, and they weren’t and dug myself a deeper trench…
I am going to be So much more careful in checking that all proper scripts are attached for now on.
Now, I am getting “Click” responses when I expect them in both scenes.
However, I am getting similar NullReferenceExceptions when moving my cursor from a walkable layer to an enemy layer. So, anytime I change layer, I get this null reference exception. This time the NullReference is in regard to “notifyLayerChangeObservers”.
I then did as you recommended above:
if (notifyLayerChangeObservers != null)
{
notifyLayerChangeObservers (newLayer);
}
since “It’s typically a bad idea to call the delegate without checking first that it isn’t null, e.g. whether it has any subscribers.” On that change, SUCCESS! Everything works as it should!
But, I am still confused as to why this happens the way it does.
So here is where my understanding of delegates falls flat. I am wondering what the notifyLayerChangeObservers is read as at different times and why it does so.
1.) CameraRaycaster+OnCursorLayerChange
UnityEngine.Debug:Log(Object)
2.) 9
UnityEngine.Debug:Log(Object)
CameraRaycaster:NotifyObserersIfLayerChanged(Int32) (at Assets/Camera and UI/CameraRaycaster.cs:97)
CameraRaycaster:Update() (at Assets/Camera and UI/CameraRaycaster.cs:68)
3.) Null
UnityEngine.Debug:Log(Object)
CameraRaycaster:NotifyObserersIfLayerChanged(Int32) (at Assets/Camera and UI/CameraRaycaster.cs:96)
CameraRaycaster:Update() (at Assets/Camera and UI/CameraRaycaster.cs:74)
4.) 9
UnityEngine.Debug:Log(Object)
CameraRaycaster:NotifyObserersIfLayerChanged(Int32) (at Assets/Camera and UI/CameraRaycaster.cs:97)
CameraRaycaster:Update() (at Assets/Camera and UI/CameraRaycaster.cs:68)
5.) NullReferenceException: Object reference not set to an instance of an object
CameraRaycaster.NotifyObserersIfLayerChanged (Int32 newLayer) (at Assets/Camera and UI/CameraRaycaster.cs:97)
CameraRaycaster.Update () (at Assets/Camera and UI/CameraRaycaster.cs:74)
6.) CameraRaycaster+OnCursorLayerChange
UnityEngine.Debug:Log(Object)
CameraRaycaster:NotifyObserersIfLayerChanged(Int32) (at Assets/Camera and UI/CameraRaycaster.cs:96)
CameraRaycaster:Update() (at Assets/Camera and UI/CameraRaycaster.cs:74)
7.) 5
UnityEngine.Debug:Log(Object)
CameraRaycaster:NotifyObserersIfLayerChanged(Int32) (at Assets/Camera and UI/CameraRaycaster.cs:97)
CameraRaycaster:Update() (at Assets/Camera and UI/CameraRaycaster.cs:57)
8.) Null
UnityEngine.Debug:Log(Object)
CameraRaycaster:NotifyObserersIfLayerChanged(Int32) (at Assets/Camera and UI/CameraRaycaster.cs:96)
CameraRaycaster:Update() (at Assets/Camera and UI/CameraRaycaster.cs:74)
9.) 5
UnityEngine.Debug:Log(Object)
CameraRaycaster:NotifyObserersIfLayerChanged(Int32) (at Assets/Camera and UI/CameraRaycaster.cs:97)
CameraRaycaster:Update() (at Assets/Camera and UI/CameraRaycaster.cs:57)
10.) NullReferenceException: Object reference not set to an instance of an object
CameraRaycaster.NotifyObserersIfLayerChanged (Int32 newLayer) (at Assets/Camera and UI/CameraRaycaster.cs:97)
CameraRaycaster.Update () (at Assets/Camera and UI/CameraRaycaster.cs:74)
On start of the game, the cursor is on a walkable layer.
So, on print-out 1 (PO1) the game checks that specific delegate (OnCursorLayerChange), and finds that the layer is valued at 9 (PO2).
At PO3, it reads Null. Why is the delegate reading null if we subscribed OnLayerChange to cameraRaycaster.notifyLayerChangeObservers on Awake? Or is the delegate only NOT null when notifying the observers, then switches back to null immediately after?
PO5 is the error that follows immediately after.
I move my cursor from a walkable layer to an enemy layer and the same thing happens over.
Just to be clear, All Issues Are Solved! I am just hoping that myself and all future confused readers can gain some insight into how this is functioning to cause errors.
I’m looking at everything now, and Wow… Thank you.
You are very welcome
On that change, SUCCESS! Everything works as it should!
Great!
But, I am still confused as to why this happens the way it does.
I’m fairly certain this comes down to the ordering of your event functions.
I don’t have a copy of your project at this time, but I’m fairly certain not all of delegates were being set up in the Awake methods, and, unless you are using script ordering, you cannot be certain of the execution order of the scripts, e.g. which of their Start methods will be called first. My belief is that you are effectively trying to access something which hasn’t been set up yet.
With regards to your printouts ordering above, are you saying that since making the changes above you are still receiving errors?
It’s obviously hard for me to diagnose from this end with only that. The video I took was of the scene mentioned after correcting the issue and no further errors were displayed. I moved the mouse from walkable, unknown, enemy etc, no errors were displayed.
Happy to take another look if you want to share the entire project files again, including any changes you have made).