Strange behaviour when inheriting parent class with Monobehaviour
Hi all,
I am having some strange behavior with Unity and my code itself. Now I can firmly say the cause is the inheritance of a parent class with Monobehaviour as it could be a bug. My issues is that sometimes my code works, sometimes it just doesn't, closing Unity often seems to break it, and only way to fix it is to delete the Prefab and drag it back into the scene. Details below.
I have the following :
Task Bar Manager- This is attached to a prefab with a Slider and text. Is going to keep track of all the tasks that need to be done.
Task Controller - This is the Parent class that inherits Monobehaviour, this class is inherited by the individual task controllers.
Individual Task Controller (Computer Controller) - This is the class that inherits Task Controller, Task Controller contains all the generic task code and the individual task controller will contain code specific to this task.
Code for each class is shown below. Further details on the issues below. ps. Sorry for weird formatting.
public class TaskBarManager : MonoBehaviour {
public static Canvas canvas;
public static Slider slider;
public static Text text;
public static int tasksTotalCount = 0;
public static int tasksCompleted = 0;
public static int tasksRemaining = 0;
private static float targetProgress = 0;
private float fillSpeed = 0.5f;
// Start is called before the first frame update
void Start()
{
canvas = GetComponent<Canvas>();
slider = GetComponentInChildren<Slider>();
text = GetComponentInChildren<Text>();
Debug.Log("Task-Bar finalised.");
}
// Update is called once per frame
void Update()
{
if (slider.value <= tasksCompleted) {
slider.value += fillSpeed * Time.deltaTime;
}
}
public static void IncrementProgress(float newProgress) {
targetProgress = newProgress;
}
public static void UpdateTasksCompleted(int Value) {
tasksCompleted = Value;
IncrementProgress(Value);
text.text = "Tasks Remaining " + tasksCompleted + " / " + tasksTotalCount;
TaskBarDidComplete();
}
public static void UpdateTaskBarMaxValue(int Value) {
tasksTotalCount = Value;
slider.maxValue = Value;
text.text = "Tasks Remaining " + tasksCompleted + " / " + tasksTotalCount;
}
public static void TaskBarDidComplete() { // - Maybe an Event would be good here?
if (tasksCompleted == tasksTotalCount) {
SceneManager.LoadScene("Game-Over");
}
}
}
public class TaskController : MonoBehaviour {
[HideInInspector] public Canvas canvas;
[HideInInspector] public CircleCollider2D detectionRadius;
[HideInInspector] public SpriteRenderer spriteRenderer;
[HideInInspector] public TaskManager taskManager;
[HideInInspector] public Slider progressBar;
[HideInInspector] public Text interactionText;
// - SETUP
public void InstantiateTask(Task task) {
canvas = gameObject.GetComponent<Canvas>();
detectionRadius = gameObject.GetComponent<CircleCollider2D>();
spriteRenderer = gameObject.GetComponent<SpriteRenderer>();
taskManager = gameObject.GetComponent<TaskManager>();
progressBar = gameObject.GetComponentInChildren<Slider>();
interactionText = gameObject.GetComponentInChildren<Text>();
ShowProgressBar(false);
SetInteractionTextPrefix(task.taskPrefix);
ShowInteractionText(false);
Debug.Log(task.taskName + " instantiated.");
}
// - USER INTERFACE HANDLING
public void ShowCanvas(bool Bool) {
canvas.gameObject.SetActive(Bool);
}
public void ShowInteractionText(bool Bool) {
interactionText.gameObject.SetActive(Bool);
}
public void SetInteractionTextPrefix(string Prefix) {
interactionText.text = "Hold 'Left-Click' to " + Prefix;
}
public void ShowProgressBar(bool Bool) {
progressBar.gameObject.SetActive(Bool);
}
// - COLLISION HANDLING
public void HandleTriggerEnter2D(Collider2D collider, Task task) {
if (!task.isCompleted) {
if (detectionRadius.IsTouching(collider.gameObject.GetComponent<BoxCollider2D>())) {
task.SetState(Task.TaskState.Hightlight);
spriteRenderer.sprite = task.activeSprite;
ShowInteractionText(true);
}
}
}
public void HandleTriggerExit2D(Collider2D collider, Task task) {
if (!task.isCompleted) {
task.SetState(Task.TaskState.Default);
spriteRenderer.sprite = task.activeSprite;
ShowInteractionText(false);
ShowProgressBar(false);
}
}
// CANVAS OVERLAY
private float targetProgress = 0;
public void CalculateProgress(Task task) {
if (progressBar.gameObject.activeInHierarchy && Input.GetButton("Fire1")) {
IncrementProgress(1f);
if (progressBar.value < targetProgress) {
progressBar.value += task.openSpeed * Time.deltaTime;
}
if (progressBar.value == 1) {
task.SetState(Task.TaskState.Complete);
spriteRenderer.sprite = task.activeSprite;
ShowProgressBar(false);
taskManager.OnTaskComplete();
if (!task.canBeCompleted) {
progressBar.value = 0;
task.isCompleted = false;
targetProgress = 0;
}
}
} else if (progressBar.gameObject.activeInHierarchy) {
ShowProgressBar(false);
ShowInteractionText(true);
}
}
public void IncrementProgress(float newProgress) {
targetProgress = progressBar.value + newProgress;
}
// COLLISION HANDLING
public void OpenTaskListener() { // NEEDS TO BE RECODED - JUST FOR TESTING
if (Input.GetMouseButtonDown(0)) {
GameObject player = GameObject.Find("Player-Prefab");
PlayerController pc = player.GetComponent<PlayerController>();
if (!pc.player.GetPlayerIsThing()) {
BoxCollider2D collider = player.GetComponent<BoxCollider2D>();
if(detectionRadius.IsTouching(collider.gameObject.GetComponent<BoxCollider2D>())) {
ShowProgressBar(true);
ShowInteractionText(false);
}
}else{
Debug.Log("Player is the Thing, cannot fix JB.");
}
}
}
}
public class ComputerController : TaskController, TaskManager {
public Task task;
void Start()
{
InstantiateTask(task);
TaskBarManager.UpdateTaskBarMaxValue(TaskBarManager.tasksTotalCount+1);
}
void Update()
{
CalculateProgress(task);
OpenTaskListener();
}
private void OnTriggerEnter2D(Collider2D collider) {
HandleTriggerEnter2D(collider, task);
}
private void OnTriggerExit2D(Collider2D collider) {
HandleTriggerExit2D(collider, task);
}
// - INTERFACE
public void OnTaskOpen() {
Debug.Log("Task has opened.");
}
public void OnTaskComplete() {
TaskBarManager.UpdateTasksCompleted(TaskBarManager.tasksCompleted+1);
}
}
As mentioned above, for some reason I get strange behavior. One of the following will happen.
The Task counter will stay at 0 / 0, or rather be set to 0 / 0. It's almost like the TaskBarManager class is loading before the individual Task classes and not getting updated.
Closing Unity seems to unset the Object Reference, I get a NullReferenceException (NullReferenceException: Object reference not set to an instance of an object TaskBarManager.UpdateTaskBarMaxValue (System.Int32 Value) (at Assets/Tasks/TaskBarManager.cs:60)) which can only be fixed by deleting the prefab and dragging it back to the scene. (This happens on re-opening Unity after closing it)
The counter will update the max value won't, i.e. it will say 0 / 2 but when one is completed the slider will act as if the Max value is only one.
Please point out if I have made any architectural mistakes in my code,
Any help is greatly appreciated,
Thanks
Answer by streeetwalker · Oct 14, 2020 at 01:58 PM
Sounds like a serialization issue. Static variables are shared by all instances of a class and cannot be serialized. This would explain why you are losing the connection to your prefab when you quit, and probably also on recompilation.
I haven't followed through your code with a fine toothed comb, but it seems like the problem has something to do with your use of static variables.
Interesting, thanks for your response. I will go through and change it from static. I want to be able to access the class from multiple individual Task controllers without creating a new instance for each one. Any suggestions?
A quick Google search for how to access non-static methods without instancing suggests using Events to listen for task completion etc.
A singleton would give you a single instance of an object that contains references to shared resources to make available to multiple objects, but you do have to create an instance of it.
Seems like you could also create a scriptableobject to contain the references to your shared objects, which itself would not need to be instantiated at all because it seems like you want to use it as a shared asset. Scriptableobjects can also contain any code you want to run except for $$anonymous$$onoBehavior functions - aside from those, I think the only thing you don't get is a convenient global object reference like you do with a singleton.
You'd just need to attach the scriptableobject ( to the classes you do instantiate) using a variable, but those variables will reference the shared scriptableobject that is not itself instantiated.
I think that would work in your situation.
Wanted to update you, I decided to go with the Singleton. It was the perfect solution as there was only ever going to be one instance of TaskBar$$anonymous$$anager, similar to that of a Game$$anonymous$$anager. I switched everything from static and used the singleton instance and everything worked no errors during runtime, no errors after restart.
If anyone else is experiencing similar issues (NullReference Exception on restarting Unity + weird code behaviour) please ensure that you check if you are using static variables / references to objects as mentioned above.
Thanks
Your answer
Follow this Question
Related Questions
Adventure interactionSystem Structure (ScriptableObject,MonoBehavior,POO?) 0 Answers
advice on resource management system architecture 2 Answers
Show variable of class that dont inherit from MonoBehaviour in Inspector 1 Answer
NullReferenceException: Object reference not set to an instance of an object in Singleton class. 1 Answer