Deadcows / MyBox

MyBox is a set of attributes, tools and extensions for Unity
http://deadcow.ru
MIT License
1.95k stars 245 forks source link

[ButtonMethod] not work in list item #231

Open CharlieDreemur opened 1 year ago

CharlieDreemur commented 1 year ago

[ButtonMethod] do works in an independent item, but it fail when come to the list item in a list/dictionary

NeilJnDa commented 5 months ago

It happened to me recently. In a button method I do something like list.Add() and list.Clear(), but the list is messed up:

  1. The list in the prefab is changed, even though I did not change the prefab.
  2. Non-null items ref in the list are cleared when clicking Play. Null items are still in the list. Hope it can be fixed soon
NeilJnDa commented 5 months ago

I think it might be related to prefabs. I found that, if the game object is not from a prefab, this button method in editor works fine, the value is not lost after playing. [ButtonMethod] void FindCollider() { m_Collider = GetComponent<Collider>(); } However, if the gameobject is from a prefab. In editor, the change made by this method is not registered. It will be lost after playing.

I guess RecordPrefabInstancePropertyModifications() or SetDirty() would help. I will try it later when I have time.

Deadcows commented 5 months ago

[ButtonMethod] do works in an independent item, but it fail when come to the list item in a list/dictionary

Unfortunately, ButtonMethod is restricted to the root of MonoBehaviour or ScriptableObject. It wouldn't work with serialized custom type, but it will be shown through DisplayInspector Attribute. So, if you will use SO or MB in a list, you can do it like that (elements of the list are child MBs): 2024-06-11_17-11-29

I know it's not a universal solution, but it is the only way, currently, to display buttons in a list. Nowadays I'm using Odin inspector to do this kind of things, it works fine along with MyBox. Extending this attribute to work with any serialized type is possible, but not in my priority. Would appreciate any contributions to the codebase πŸ˜…πŸ™ŒπŸ»


I think it might be related to prefabs. I found that, if the game object is not from a prefab, this button method in editor works fine, the value is not lost after playing. [ButtonMethod] void FindCollider() { m_Collider = GetComponent<Collider>(); } However, if the gameobject is from a prefab. In editor, the change made by this method is not registered. It will be lost after playing.

Yep, you should definitely mark dirty any object, if you change its serialized data in the code during edit-time. Unity doesn't know that you changed the m_Collider field or resized the serialized list, so it won't save changes on the disc and they will be wiped out on domain reload. The reason why changes weren't lost the first time is probably because you changed something else via inspector (this way the object was marked dirty internally) and all serialized data was successfully saved πŸ€”. Something like this as a rule of thumb:

#if UNITY_EDITOR
[ButtonMethod] void FindCollider() { 
    m_Collider = GetComponent<Collider>();
    UnityEditor.EditorUtility.SetDirty(gameObject);
 }
#endif
NeilJnDa commented 5 months ago

In this page: https://docs.unity3d.com/ScriptReference/EditorUtility.SetDirty.html

If the object may be part of a Prefab instance, you additionally need to call PrefabUtility.RecordPrefabInstancePropertyModifications to ensure a Prefab override is created.

A prefab instance works with this piece:

#if UNITY_EDITOR
[ButtonMethod] void FindCollider() { 
    Undo.RecordObject(this, "Assign m_Collider");
    m_Collider = GetComponent<Collider>();
    PrefabUtility.RecordPrefabInstancePropertyModifications(this);
 }
#endif

Undo.RecordObject() would be better than UnityEditor.EditorUtility.SetDirty() Otherwise I need to use SerializedProperty system, but I am not really into that more complex way. I recommend you add some more notes to https://github.com/Deadcows/MyBox/wiki/Attributes#buttonmethod so that others know it as well. πŸ˜„

NeilJnDa commented 5 months ago

@Deadcows Also, would you like if add these two lines to ButtonMethodAttribute.cs?

private void InvokeMethod(Object target, MethodInfo method)
{
    Undo.RecordObject(target, method.Name);   //New Line
    var result = method.Invoke(target, null);
    PrefabUtility.RecordPrefabInstancePropertyModifications(target);  ////New Line
    if (result != null)
    {
        var message = $"{result} \nResult of Method '{method.Name}' invocation on object {target.name}";
        Debug.Log(message, target);
    }
}

I am not pretty sure this would fit every cases of this issueπŸ˜‚, at least I tried myself it works with scene objects/ prefab instance/ modification of other objects in the scene.

Deadcows commented 5 months ago

I don't like the idea that it always makes the scene dirty. I tried that and it made some undesired behaviors in my previous project πŸ€” (several game designers pushed a bunch of changed scenes in vcs causing a lot more conflicts that it should πŸ˜…)

Maybe it's better to dirty it if the "result" is not null and cast to "true"? So, if ButtonMethod returns bool and explicitly indicates that the scene should be marked dirty as a result of its invocation πŸ€”

So, usage will be like this:

[ButtonMethod]
private bool FixTheName() {
    var newName = GetValidName();
    if (newName == name) return false;
    name = newName;
    return true;
}

Would be optional, and easily explained in docs. Nicely combined with ContentsMatch api... What do you think?

NeilJnDa commented 5 months ago

However the return type of this method is restricted to bool πŸ€”. If a method written before returns bool, after this update may cause unexpected results. Would an optional parameter of this attribute help? For example:

//ButtonMethodAttribute(ButtonMethodDrawOrder drawOrder = ButtonMethodDrawOrder.AfterInspector, bool recordModifications = false)
[ButtonMethod( ButtonMethodDrawOrder.AfterInspector, true)]
private void Method(){
    //...
}

If recordModifications is explicitly set to true, then these two methods can be called before and after invoking this button method:

...
Undo.RecordObject(target, method.Name);   //New Line before invoking
PrefabUtility.RecordPrefabInstancePropertyModifications(target);  //After invoking, necessary for prefabs
...

Nice discussion! Also, I like your games! 😊

Deadcows commented 5 months ago

Yep, backward compatibility is a thing I always forget about πŸ˜… I don't like the optional parameter thing, because the decision to make dirty often comes in the body of the method. Why not create a custom type as a flag for a ButtonMethod, then?

This will allow to keep the default behavior with logging for all other types πŸ‘πŸ» like here (not sure about the naming though):

public enum MakeDirty { True, False } //Part of MyBox
...
[ButtonMethod]
private MakeDirty Method(){
    //...
    return MakeDirty.True;
}

btw, is it safe to call RecordPrefabInstancePropertyModifications on any objects or we should check if target object is part of a prefab first? And it also probably should only be called during prefab-editing-mode, to not force the changes made on prefab instance πŸ€”


Nice discussion! Also, I like your games! 😊

Thank you, mate! Working hard on the third project, so not have that many time to spend on MyBox now. But I'll make a big update when sort all the cool things I made during the work on The Bookwalker πŸ™‚

NeilJnDa commented 5 months ago

@Deadcows I realized that Undo.RecordObject(target, method.Name) is called before invoking the method, so probably the return value of the method can not decide if it should record undo or set dirty (set dirty does not record undo).

private void InvokeMethod(Object target, MethodInfo method)
{
    Undo.RecordObject(target, method.Name);
    var result = method.Invoke(target, null);
    PrefabUtility.RecordPrefabInstancePropertyModifications(target);
    if (result != null)
    {
        var message = $"{result} \nResult of Method '{method.Name}' invocation on object {target.name}";
        Debug.Log(message, target);
    }
}

From my own experience, most of time a button method does something about changing serialized values (from Monobehavior, ScriptableObject or a serializable class). I would say by default RecordObject() and RecordPrefabInstancePropertyModifications() should be called to support most cases. And also to keep it neat, I would recommend this way:


//In mybox
//ButtonMethodAttribute(ButtonMethodDrawOrder drawOrder = ButtonMethodDrawOrder.AfterInspector, bool recordModifications = true)  

...
//Be default, record modifications.
[ButtonMethod]
private void Method(){
    //...
}

//If a developer really does not want it to be recorded, note that every change will be lost after playing.
[ButtonMethod( ButtonMethodDrawOrder.AfterInspector, false)]
private void MethodNoModification(){
    //...
}

btw, is it safe to call RecordPrefabInstancePropertyModifications on any objects or we should check if target object is part of a prefab first? And it also probably should only be called during prefab-editing-mode, to not force the changes made on prefab instance πŸ€”

I tried in my project, RecordPrefabInstancePropertyModifications() is safe to call for a gameobject in scene. I think Unity does some checking internally.

What would you think?❀️