Unity resets parameter value set with Editor script on play - c#

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();
}

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.

How to perform an action after key pressed?

I tried to hide a text in Unity after caps was pressed, but it doesn't work, it stops before "while".
I'm quite a not up to par programmer, so anyone more experienced?
private float TurnOffInfoText()
{
bool IsCapsPressed = Input.GetKeyDown(KeyCode.CapsLock);
while (IsCapsPressed == true)
{
EndOfGameText.enabled = false;
}
return 0;
}
Why does this even return a value?
Also your while loop would completely freeze the entire App and even the Unity Editor application! Within the loop the IsCapsPressed value is never ever changed!
I don't see where your method is called from but if you never experienced a freeze so far then "luckily" the key never went down in the same frame so far.
Usually you would rather poll the input every frame. By a simple look into the API for Input.GetKeyDown:
private void Update ()
{
if(Input.GetKeyDown(KeyCode.CapsLock))
{
EndOfGameText.enabled = false;
}
}

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

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

Cancelling Coroutine when Home button presseddown or returned main menu

some pretext of what I am doing ; I am currently locking down my skill buttons via setting interactable = false in coroutines. Showing text of remaning seconds via textmeshpro and setting them deactive when countdown is over. But I am having problem when home button is pressed/ returned main menu. I would like to refresh my buttons cooldowns and stop coroutines when its pressed. But it is staying in lock position.
this is my cooldown coroutine
static List<CancellationToken> cancelTokens = new List<CancellationToken>();
...
public IEnumerator StartCountdown(float countdownValue, CancellationToken cancellationToken)
{
try
{
this.currentCooldownDuration = countdownValue;
// Deactivate myButton
this.myButton.interactable = false;
//activate text to show remaining cooldown seconds
this.m_Text.SetActive(true);
while (this.currentCooldownDuration > 0 && !cancellationToken.IsCancellationRequested)
{
this.m_Text.GetComponent<TMPro.TextMeshProUGUI>().text = this.currentCooldownDuration.ToString(); //Showing the Score on the Canvas
yield return new WaitForSeconds(1.0f);
this.currentCooldownDuration--;
}
}
finally
{
// deactivate text and Reactivate myButton
// deactivate text
this.m_Text.SetActive(false);
// Reactivate myButton
this.myButton.interactable = true;
}
}
static public void cancelAllCoroutines()
{
Debug.Log("cancelling all coroutines with total of : " + cancelTokens.Count);
foreach (CancellationToken ca in cancelTokens)
{
ca.IsCancellationRequested = true;
}
}
void OnButtonClick()
{
CancellationToken cancelToken = new CancellationToken();
cancelTokens.Add(cancelToken);
Coroutine co;
co = StartCoroutine(StartCountdown(cooldownDuration, cancelToken));
myCoroutines.Add(co);
}
this is where I catch when home button pressed/returned main menu. when catch it and pop pauseMenu
public void PauseGame()
{
GameObject menu = Instantiate(PauseMenu);
menu.transform.SetParent(Canvas.transform, false);
gameManager.PauseGame();
EventManager.StartListening("ReturnMainMenu", (e) =>
{
Cooldown.cancelAllCoroutines();
Destroy(menu);
BackToMainMenu();
EventManager.StopListening("ReturnMainMenu");
});
...
I also stop time when game on the pause
public void PauseGame() {
Time.timeScale = 0.0001f;
}
You are using CancellationToken incorrectly in this case. CancellationToken is a struct that wraps a CancellationTokenSource like this:
public bool IsCancellationRequested
{
get
{
return source != null && source.IsCancellationRequested;
}
}
Because it's a struct, it gets passed around by value, meaning the one you store in your list is not the same instance as the one that your Coroutine has.
The typical way to handle cancellation is to create a CancellationTokenSource and pass its Token around. Whenever you want to cancel it, you simply call the .Cancel() method on the CancellationTokenSource. The reason for it being this way is so that the CancellationToken can only be cancelled through the 'source' reference and not by consumers of the token.
In your case, you are creating a token with no source at all so I would suggest making the following changes:
First of all, change your cancelTokens list to be a:
List<CancellationTokenSource>
Next, change your OnButtonClick() method to look like this:
public void OnButtonClick()
{
// You should probably call `cancelAllCoroutines()` here
cancelAllCoroutines();
var cancellationTokenSource = new CancellationTokenSource();
cancelTokens.Add(cancellationTokenSource);
Coroutine co = StartCoroutine(StartCountdown(cooldownDuration, cancellationTokenSource.Token));
myCoroutines.Add(co);
}
And lastly, change your cancelAllCoroutines() method to this:
public static void CancelAllCoroutines()
{
Debug.Log("cancelling all coroutines with total of : " + cancelTokens.Count);
foreach (CancellationTokenSource ca in cancelTokens)
{
ca.Cancel();
}
// Clear the list as #Jack Mariani mentioned
cancelTokens.Clear();
}
I would suggest reading the docs on Cancellation Tokens or alternatively, as #JLum suggested, use the StopCoroutine method that Unity provides.
EDIT:
I forgot to mention that it is recommended that CancallationTokenSources be disposed of when no longer in use so as to ensure no memory leaks occur. I would recommend doing this in an OnDestroy() hook for your MonoBehaviour like so:
private void OnDestroy()
{
foreach(var source in cancelTokens)
{
source.Dispose();
}
}
EDIT 2:
As #Jack Mariani mentioned in his answer, multiple CancellationTokenSources is overkill in this case. All it would really allow you to do is have more fine-grained control over which Coroutine gets cancelled. In this case, you are cancelling them all in one go, so yeah, an optimisation would be to only create one of them. There are multiple optimisations that could be made here, but they are beyond the scope of this question. I did not include them because I felt like it would bloat this answer out more than necessary.
However, I would argue his point about CancellationToken being 'mostly intended for Task'. Pulled straight from the first couple of lines in the MSDN docs:
Starting with the .NET Framework 4, the .NET Framework uses a unified model for cooperative cancellation of asynchronous or long-running synchronous operations. This model is based on a lightweight object called a cancellation token
CancellationTokens are lightweight objects. For the most part, they are just simple Structs that reference a CancellationTokenSource. The 'overhead' that is mentioned in his answer is negligible and, in my eyes, totally worth it when considering readability and intention.
You could pass a load of booleans around with indices or subscribe to events using string literals and those approaches would work.
But at what cost? Confusing and difficult-to-read code? I would say not worth it.
The choice is ultimately yours though.
MAIN ISSUE
In your question you just use the bool cancellationToken.IsCancellationRequested and no other functionalities of Cancellation Token.
So, following the logic of your method, you might just want to have a List<bool> cancellationRequests and pass them using ref keyword.
Still, I would not go ahead with that logic nor with the logic proposed by Darren Ruane, because they have one main flaw.
FLAW => these solutions keep adding things to 2 lists cancelTokens.Add(...) and myCoroutines.Add(co), without ever clearing them.
public void OnButtonClick()
{
...other code
//this never get cleared
cancelTokens.Add(cancelToken);
...other code
//this never get cleared
myCoroutines.Add(co);
}
If you want to go down this way you could remove them manually, but it's tricky, because you never know when they will be required (the Coroutine can be called many frames after CancelAllCoroutines method).
SOLUTION
Use a static event instead of a list
To remove the list and make the class even more decoupled you might use a static event, created and invoked in the script where you call the PauseGame method.
//the event somewhere in the script
public static event Action OnCancelCooldowns;
public void PauseGame()
{
...your code here, with no changes...
EventManager.StartListening("ReturnMainMenu", (e) =>
{
//---> Removed ->Cooldown.cancelAllCoroutines();
//replaced by the event
OnCancelCooldowns?.Invoke();
...your code here, with no changes...
});
...
You will listen to the static event in your coroutine.
public IEnumerator StartCountdown(float countdownValue, CancellationToken cancellationToken)
{
try
{
bool wantToStop = false;
//change YourGameManager with the name of the script with your PauseGame method.
YourGameManager.OnCancelCooldowns += () => wantToStop = true;
...your code here, with no changes...
while (this.currentCooldownDuration > 0 && !wantToStop)
{
...your code here, with no changes...
}
}
finally
{
...your code here, with no changes...
}
}
EASIER SOLUTION
Or you might just stay simple and use MEC Coroutines instead of Unity ones (they are also more performant ).
MEC Free offers the tag functionality that solves the problem entirely.
SINGLE COROUTINE START
void OnButtonClick() => Timing.RunCoroutine(CooldownCoroutine, "CooldownTag");
STOP ALL COROUTINE with a specific tag
public void PauseGame()
{
...your code here, with no changes...
EventManager.StartListening("ReturnMainMenu", (e) =>
{
//---> Removed ->Cooldown.cancelAllCoroutines();
//replaced by this
Timing.KillCoroutines("CooldownTag");
...your code here, with no changes...
});
...
Further notes on tags (please consider that MEC free has only tags and not layers, but you require just tags in your use case).
EDIT:
After some thought I just decided to remove the details of the bool solution, it might confuse the answer and it was going beyond the scope of this question.
Unity has StopCoroutine() specifically for ending Coroutines early.
You will want to create a function that you can call on every skill button object when you want to reset them all like this:
void resetButton()
{
StopCoroutine(StartCountdown);
this.currentCooldownDuration = 0;
this.m_Text.SetActive(false);
this.myButton.interactable = true;
}

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).

Categories