How do I trace the source of the call "OnValidate()" - c#

I want a script to have radio buttons for booleans and it seems like OnValidate() would be the perfect way to do that. However, i would need to trace what value was changed in the inspector and put a check for the identifier but I couldn't find the solution for the tracing part. How do i know what value was changed for OnValidate() to be called?

As said in the comments might not be the most "beautiful" solution but I would do it e.g. like
// These are the fields in the Inspector
// changing any via the Inspector will Invoke OnValidade
[SerializeField] private bool bool1;
[SerializeField] private bool bool2;
// These are private and will be used to check what was changed
private bool _oldBool1;
private bool _oldBool2;
private void OnValidate()
{
if(bool1 != _oldBool1)
{
// bool1 was changed
if(bool1)
{
// Probably: set all other values to false
}
else
{
// Probably check if all other values are false, if so this may not be false
}
}
if(bool2 != _oldBool2)
{
// bool2 was changed
if(bool2)
{
// Probably: set all other values to false
}
else
{
// Probably check if all other values are false, if so this may not be false
}
}
// Etc
// And finally store the new values
_oldBool1 = bool1;
_oldBool2 = bool2;
// Etc
}
Afaik the changes via script of the serialized fields should not Invoke another OnValidate, only changes via the Inspector or the first time the asset is loaded.
This function is called when the script is loaded or a value is changed in the Inspector (Called in the editor only).
You could probably also work with a List/Array instead of individual fields of course
Typed on smartphone so can't test it right now but I hope the idea gets clear

Related

In Unity, how can I remove multiple components when removing one in editor?

I have a component called ContainerDescriptor. When I remove it by right click and then left click remove component, I'd like that it removes other components as well, referenced in the script.
I'm currently using OnDestroy() in ContainerDescriptor.cs.
I'm also using the [ExecuteAlways] attribute so OnDestroy() is called in editor mode as well.
Say I have a reference towards another component called attachedContainer and I want to remove it when destroying containerDescriptor. Currently I'm doing the following.
in ContainerDescriptor.cs :
[ExecuteAlways]
public class ContainerDescriptor : MonoBehaviour
{
// ... some code
public AttachedContainer attachedContainer;
private void OnDestroy()
{
if (Application.isPlaying)
{
Destroy(attachedContainer);
}
else
{
DestroyImmediate(attachedContainer, true);
}
}
// ... some more code
}
It works well in Editor mode but when I press play I get the following error every time :
Destroying object multiple times. Don't use DestroyImmediate on the
same object in OnDisable or OnDestroy.
But Destroy() only works in play mode ! So I don't see which options are left for me.
Ideally, I could remove those components by calling DestroyImmediate() somewhere else, in some method OnRemoveComponent called when selecting remove Component in Editor, but I can't find anything like that in documentation.
If it's relevant, I'm using Unity 2019.3.
It might help to check if the object you're going to destroy is not destroyed already?
else
{
if(attachedContainer!=null)
{
DestroyImmediate(attachedContainer, true);
}
}
Please note that I haven't tested the code above. I hope you get the point.
Using a custom editor does the trick. I declare as references every component I want to delete in the custom editor script. I assign the references in containerDescriptor to the references in containerDescriptorEditor. I then use the onDestroy of the custom editor :
In containerDescriptorEditor.cs :
public class ContainerDescriptorEditor : Editor
{
private ContainerDescriptor containerDescriptor;
private AttachedContainer attachedContainer;
private AttachedContainerGenerator attachedContainerGenerator;
private ContainerInteractive containerInteractive;
private VisibleContainer visibleContainer;
// ... some code
public void OnEnable()
{
containerDescriptor = (ContainerDescriptor)target;
attachedContainer = containerDescriptor.attachedContainer;
containerInteractive = containerDescriptor.containerInteractive;
visibleContainer = containerDescriptor.visibleContainer;
}
// ... some more code
private void OnDestroy()
{
if(containerDescriptor == null)
{
RemoveContainer();
}
}
private void RemoveContainer()
{
DestroyImmediate(attachedContainer, true);
DestroyImmediate(attachedContainerGenerator, true);
DestroyImmediate(containerInteractive, true);
DestroyImmediate(visibleContainer, true);
}
// ...
}
This works for me.
Here's the script I wrote to resolve the issue.
https://github.com/saadasghar96/Unity/blob/master/Utility/MultiComponentCopierCleaner.cs
Go under Earth People Studio > Multi-Component Cleaner
If you leave the 'component' field empty in the Multi-Component Cleaner, it will remove every type of component. Type in "rigidbody" (casings don't matter as long as the component is partially named correctly) and the script will do the rest.
Be sure to back up the object you're cleaning in case things go wrong.

Unity resets parameter value set with Editor script on play

I've set up a very simple Editor script in Unity 2020.2.1f1 that, upon pressing an Inspector button, should change the value of a specified parameter to a value set in the code.
public override void OnInspectorGUI()
{
DrawDefaultInspector();
StateObject s = (StateObject)target;
if (s.objID == 0)
{
if (GUILayout.Button("Generate Object ID"))
{
GenerateID(s);
}
}
}
public void GenerateID(StateObject s)
{
s.objID = DateTimeOffset.Now.ToUnixTimeSeconds();
}
This all works like it's supposed to. I press the button, the correct number appears in the field, and I'm happy. However, once I switch to Play mode, the value resets to the prefab default and remains that way even when I switch Play mode off.
Am I missing some ApplyChange function or something?
(EDIT: This works, but isn't as good as the accepted answer.)
Well, yes, I am in fact missing some sort of ApplyChange function.
I don't know how I missed it, but I was looking for this:
EditorUtility.SetDirty(target);
So, in my script, I would just edit the GenerateID function:
public void GenerateID(StateObject s)
{
s.objID = DateTimeOffset.Now.ToUnixTimeSeconds();
EditorUtility.SetDirty(s);
}
And I am posting it here in case anyone runs into the same issue, that way they hopefully won't have to spend as much time looking for a solution before being reminded that SetDirty is a thing.
In my eyes better than mixing in direct accesses from Editor scripts and then manually mark things dirty rather go through SerializedProperty and let the Inspector handle it all (also the marking dirty and saving changes, handle undo/redo etc)
SerializedProperty id;
private void OnEnable()
{
id = serializedObject.FindProperty("objID");
}
public override void OnInspectorGUI()
{
DrawDefaultInspector();
// Loads the actual values into the serialized properties
// See https://docs.unity3d.com/ScriptReference/SerializedObject.Update.html
serializedObject.Update();
if (id.intValue == 0)
{
if (GUILayout.Button("Generate Object ID"))
{
id.intValue = DateTimeOffset.Now.ToUnixTimeSeconds();
}
}
// Writes back modified properties into the actual class
// Handles all marking dirty, undo/redo, etc
// See https://docs.unity3d.com/ScriptReference/SerializedObject.ApplyModifiedProperties.html
serializedObject.ApplyModifiedProperties();
}

Is it possible to return a variable type to the script that called a coroutine?

Alright, I have a Dialogue system in place in my game. It is pretty simple in its design. Each dialogue contains a list of nodes which would be what the NPC would say and for each node there is also a list of options that the player can choose from either to end the conversation or continue on. The dialogue part works flawlessly. What I am trying to do is combine it with a questing system. Before I get too far into my questing system I need to figure out a way to connect my dialogue system to my questing system or NPC (preferably my NPC). The way my dialogue system is set up is with a Singleton pattern and each NPC would just call a method on it that starts a dialogue with a player based on its local dialogue variable.
I've been sitting here thinking about how I can pass a value from my dialogue manager to my NPC but, considering that my run method is a Coroutine I can't figure out how to return that value after I exit the coroutine if I need to. I feel like this should be possible but, I really can't think of a way to do this. Any help would be appreciated.
An ideal situation is to get the RunDialogue method to return a variable type(bool?), but only after EndDialogue has been called from within the run method. If it returns true then assign the quest otherwise do nothing.
From DialogueManager:
public void RunDialogue(Dialogue dia)
{
StartCoroutine(run(dia));
}
IEnumerator run(Dialogue dia)
{
DialoguePanel.SetActive(true);
//start the convo
int node_id = 0;
//if the node is equal to -1 end the conversation
while (node_id != -1)
{
//display the current node
DisplayNode(dia.Nodes[node_id]);
//reset the selected option
selected_option = -2;
//wait here until a selection is made by button click
while (selected_option == -2)
{
yield return new WaitForSeconds(0.25f);
}
//get the new id since it has changed
node_id = selected_option;
}
//the user exited the conversation
EndDialogue();
}
From NPC:
public override void Interact()
{
DialogueManager.Instance.RunDialogue(dialogue);
}
There is a way to make a coroutine return a value. Requires some nesting, you can have a look at this video if you want to go this way: Unite 2013 - Extending Coroutines # 20m38s on "adding return values"
Otherwise, you can pass a callback to the coroutine. This will probably do it for you.
Add some function at a point where it makes sense (e.g. the NPC):
public void OnGiveQuest()
{
// Add the quest
}
Add it to the dialogue call:
public override void Interact()
{
DialogueManager.Instance.RunDialogue(dialogue, OnGiveQuest);
}
Then change your RunDialogue and run to take a callback:
public void RunDialogue(Dialogue dia, System.Action callback = null)
{
StartCoroutine(run(dia, callback));
}
Now, for the coroutine, you either pass the callback further to EndDialogue or handle it in here too after the end call.
IEnumerator run(Dialogue dia, System.Action callback = null)
{
...
//the user exited the conversation
EndDialogue();
if(callback != null)
callback();
}
Now, I made it the way that you would only add the callback if you want to start a quest. Otherwise you just leave it out (default value of null).

Unity Click button

I'm trying to remove a gameObject from the world and a list using:
Destroy(obj);
itemsInInv.Remove(obj.gameObject);
But for some reason, it's reading my list.count as 0 every time I try to reference it threw a click event. If I use a key down and remove a specific item it works fine. But when I try to read my list threw a click event it returns 0 instead of the amount of items actually in the array
Like I can push 'Z' and get the Debug.Log(itemsInInv.Count) will say 15. Then I'll click the item to try and run the same Debug.Log(itemsInInv.Count) and it returns 0.
I've tried using the built in interface on the unity program. I tried using
items.GetComponent<Button>().onClick.AddListener(() => { useItem(); });
Which actually did work 100% fine for a little while then for some reason stopped working and won't work again no matter what I do lol.
I've tried using:
void OnDestroy()
{
manager.GetComponent<UIManager>().Remove(this.gameObject);
Debug.Log("remove" + manager.GetComponent<UIManager>().itemsInInv.Count);
}
public void OnPointerClick(PointerEventData eventData)
{
Destroy(gameObject);
}
Also tried doing the remove in the same function as the click. Same problem. Idk how else this is possible to do it? Even if I create a boolean and have it set to false, then use the button to set it too true, it won't read as true when debugged.
What I'm pretty much asking is, how do I add a list of buttons to the screen. Then delete the SAME button I clicked on from the screen and the list.
Soo this is what i did to get it too work....
i added this script onto my item thats being clicked
public void OnPointerClick(PointerEventData eventData)
{
EventSystem.current.SetSelectedGameObject(gameObject);
Debug.Log("hey"+ EventSystem.current.currentSelectedGameObject);
}
then in my other script im running
items.GetComponent<Button>().onClick.AddListener(() => { useItem(); });
which runs
public void useItem()
{
Invoke("itemUsed", 0.25f);
}
which runs after 0.25
void itemUsed()
{
Debug.Log(EventSystem.current.currentSelectedGameObject);
deleteItem(EventSystem.current.currentSelectedGameObject);
sortItems();
}
I had to do the invoke because it was calling useItem, before the currently selected was being selected
Also my event System was a child of my canvas which made my gameManager unable to access it the way i was
Dont use a list. Use a Dictionary.
public static Dictionary<string, GameObject> dict = new Dictionary<string, GameObject>();
then you can use the gameobjects name.
foreach(GameObject r in FindObjectsOfType(typeof(GameObject)))
{
dict.Add(r.name, r);
}
Then you can do
Destroy(gameObject.name);
dict.Remove(gameObject.name);

get and Set not working

I am writing the following get and set for validating an input from a Text Box. Basically it is supposed to check if the user has entered all of the values.
When I leave the TextBoxes empty , it does nothing and shows a '0' in output where that variable was being used. It does however show the system generated exception and stops the execution, but I wonder why doesn't it validate the input through the properties?
Here is my code:
public double RecoDoseSize
{
get
{
return recoDoseSize;
}
set
{
if (!(value>0))
{
MessageBox.Show("Please Enter the recommended dose size for this product");
textBox8.Focus();
}
recoDoseSize = value;
}
}
private void Submit2_Click(object sender, RoutedEventArgs e)
{
TotalContentProduct = double.Parse(textBox7.Text);
recoDoseSize = double.Parse(textBox8.Text);
NoOfDosespUnit = TotalContentProduct/recoDoseSize;
}
You are setting recoDoseSize, the backing field, not RecoDoseSize, the property which has your code in it. Thus, your code isn't executed. You need to change the second line of your method body to
RecoDoseSize = double.Parse(textBox8.Text);
(note the capital R).
Other have given the correct answer to the question as stated. Namely that you should call the uppercased RecoDoseSize if you want to use the getter/setter.
However it is extremely bad practice to show a message box inside the setter, because it violates the Principle of Least Surprise.
When someone looks at the line RecoDoseSize = double.Parse(textBox8.Text); it is not at all obvious that this operation could cause a message box to appear.
There are occasionally exceptions where it does make sense to have a setter trigger UI changes (for instance the Visible property on controls) however the default should always be to not do this unless you are sure it will be more confusing to not do so (for instance it would be surprising if you set Visible = false however it was still visible).
Regarding your comment on how you should implement it, the checking should be done in the click handler and the property can just be an auto-property, like so:
public double RecoDoseSize { get; set; }
private void Submit2_Click(object sender, RoutedEventArgs e)
{
TotalContentProduct = double.Parse(textBox7.Text);
double enteredSize;
if (!double.TryParse(textBox8.Text, out enteredSize) || enteredSize <= 0)
{
MessageBox.Show("Please Enter the recommended dose size for this product");
textBox8.Focus();
return;
}
RecoDoseSize = enteredSize;
NoOfDosespUnit = TotalContentProduct / recoDoseSize;
}
You'll want to use TryParse because with Parse you'll get an error if the text isn't a valid double. What TryParse does is return true or false depending on whether it succeeded, and it populates the out parameter with the result if it's successful.
So what this does is if it either failed to parse the result, or the result is <= 0 it shows the message box. In that case it also returns from the method so the rest of it isn't executed. Alternatively the rest of the method could be in an else block in which case the return isn't needed. It's a matter a style which way is preferred.
You're never actually using the getter/setter. You are using the actual field name: recoDoseSize directly. Change it to RecoDoseSize.
private void Submit2_Click(object sender, RoutedEventArgs e)
{
TotalContentProduct = double.Parse(textBox7.Text);
RecoDoseSize= double.Parse(textBox8.Text);
NoOfDosespUnit = TotalContentProduct/recoDoseSize;
}
You shouldn't be handling focus in your set statement.
Also, you need to make sure that value is not null, otherwise you can't compare it to anything (greater-than, etc.).

Categories