- Home /
Serializable class with access to its Monobehaviour host
Hello everyone, I'm trying to create a Serializable class to be used in the inspector, I need to access the hosting monobehaviour for this class, but sense it's created by unity and serialized automatically you can't make use of custom constructors to pass what you want.
I will give you a simple example, I'm making a multipier system for damage and so, the multiplier is not just a number but a class that has functionality, limits, some calculations method fit for the game
[Serializable]
public class Multiplier
{
private float val = 1.0f;
private Action<MonoBehaviour> action;
public float Value
{
get { return val; }
set
{
val = value;
action?.Invoke(theMono);
}
}
}
the multiplier can be basically used for anything and it can be put inside any monobehaviour, the multiplier has an event to announce that its value has changed but I need to send the changing monobehaviour with the event, as the value alone is useless
another example is that I'm making inventory with serializable class InventorySlot, that has custom property drawer and so, I need to invoke inventory updated event when the slot changes, but the slot doesn't have access to the inventory
is there a way to achieve this?
Answer by Bunny83 · Aug 02, 2018 at 11:01 PM
That is simply not possible. A class instance is a standalone object. It may be referenced by several variables of different objects. There is no "owner". If you actually want a two way relationship you may want to make a component out of your class.
Another option is to pass the object reference you're interested in as parameter. Of course you can't use a property in that case but have to use an explicit method.
Finally you could implement the ISerializationCallbackReceiver in the hosting class and just set a field in your Multiplier instance in OnAfterDeserialize.
Though i still don't quite understand the usage of that class. Keep in mind that custom serializable classes do not support inheritance when it comes to serialization. So what is the point of a seperate class here? Who is subscribing to your "action"? Doesn't a closure work just fine? How do you subscribe to action?
thank you for your answer, I should have probably name the action to make it clear, it's supposed to be an event for "On$$anonymous$$ultiplierChanged", for example I can use it as character.$$anonymous$$eleeDamage$$anonymous$$ultiplier.On$$anonymous$$ultiplierChanged += DoSomething
the ISerializationCallbackReceiver solution seems good, I'll try it, but this will have to be on a defined type like in my inventory example, but the multiplier can be added to many other scripts
this really help, thank you very much
Answer by JVene · Aug 02, 2018 at 11:09 PM
While I'm not 100% certain I fully understand all that you require, it seems you have two problems to solve.
First, since you can't create a custom constructor, you can't obtain a linkage between the owning MonoBehaviour and these classes, be it slot, multiplier or whatever. You already realize there's no way to discover an instance automatically (how could one know what instance to find), you have to break this deadlock by using an initialization in Awake or Start of the MonoBehaviour, where you can perform what I'll call post construction - that which you would have performed by using a custom constructor for Multiplier, but must do so when MonoBehaviour can do that for you. You could only perform some form of lazy initialization of the connection between multiplier and the owning class is if it were a singleton, and could be discovered from a static value lazily initialized.
Since that's not an option, you have to use Awake or Start, or some other lazy initialization means discovered from the MonoBehaviour that owns these objects.
That said, I'll turn to the function call from within the multiplier, slot or whatever class must "callback".
You could pass "this" from MonoBehaviour to multiplier, which would give it an instance upon which to call any member of the MonoBehaviour derivative. That assumes the function is always the same, but the generic nature of multiplier suggests this function is different for each use. For that you should consider a delegate. A delegate combines the notion of an instance and the member function to be called. It appears like a member variable you set, but calls like a function. Google provides.
thank you for your answer, I thought about the tantalizer already, but the problem is the size of the inventory is dynamic, it grow or shrink in the game, also I could add more slots for at run-time for testing and I can drop items in in it from the inspector, so if I increased the array size at run-time the start
function won't help here.
Also in the multiplier case it can be hosted in different scripts, so probably the delegate is fine, but I'm trying to be more generic in the solution,but as you can see they are 2 different problems, delegates might work for one but not for the other
for example, when I drop an item in an inventory slot it's added correctly but the inventoryUpdated event won't be fired, because it was passed to the slot directly not through the inventory class (usually inventory.AddItem() should fire the event)
this was really helpful, thanks
I have no idea what a tantalizer is ;)
You should be aware that we can only answer based on the information in the question, and the notion of a callback via delegate requires some design with respect to placement. Indeed, most of object oriented development is about decided what does something, where that code belongs. If, for example, you're creating inventory items in code at runtime, that is rather obviously the moment in which a delegate could be paired, since it isn't co$$anonymous$$g from strea$$anonymous$$g during initialization. However, since the inventory may be a large number of items, it may be more appropriate for the operating container to control the notification by insisting that such updates go through the container manager which then provides the callback, rather than make the callback come from the inventory item. That is to say, the notion that something should be passed to the slot directly is questionable design, because it allows a bypass of control. Where the multiplier refers to different classes (the word scripts is not known in C# program$$anonymous$$g, a script is just a text file, the class is the key concept), you may want to make the multiplier class a generic so it can automatically adapt to any class and any relevant function in that class.