Search Unity

Referencing code in a scriptable object

Discussion in 'Scripting' started by cranky, Feb 16, 2017.

  1. cranky

    cranky

    Joined:
    Jun 11, 2014
    Posts:
    180
    Hey, I'm not really sure how to describe this, so I'll use an example:

    Let's say I have a buff/debuff system. I want to set up each buff/debuff as a ScriptableObject. There will be lots of values to set that are common, such as duration, icon, etc. However, I'd like to specify code to execute on certain events, such as OnApply, OnExpire, OnDispel, etc. UnityEvents don't seem like a workable solution as I cannot reference an object which contains the code.

    My idea would be to create an abstract class with virtual methods taking game state objects as parameters. Then I can reference classes which inherit from this abstract class in the ScriptableObject. However, this doesn't appear possible.

    I suppose I could use a string to specify the type name and create an instance via reflection... but I'm assuming the performance would be abysmal (among other potential problems) and would only rely on this as a last resort.

    Any ideas? Thanks!
     
  2. takatok

    takatok

    Joined:
    Aug 18, 2016
    Posts:
    1,496
    Not sure what you mean by this. I was able to send events back and forth between a ScriptableObject and a MonoBehaviour

    Code (CSharp):
    1. using System;using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEngine.Events;
    5.  
    6. public class MySO : ScriptableObject
    7. {
    8.     UnityEvent soEventHandler;
    9.     void OnEnable()
    10.     {
    11.         GameObject controller = GameObject.FindWithTag("GameController");
    12.         EventController script = controller.GetComponent<EventController>();
    13.         script.AddListener(HandleMouseClick);
    14.         soEventHandler = new UnityEvent();
    15.     }
    16.  
    17.     public void AddListener(UnityAction method)
    18.     {
    19.         soEventHandler.AddListener(method);
    20.     }
    21.     public void HandleMouseClick(float x, float y)
    22.     {
    23.         Debug.Log("You pressed the mouse at " + x + "," + y);
    24.         soEventHandler.Invoke();
    25.     }
    26.  
    27. }
    28.  

    Code (CSharp):
    1. using System;using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEngine.Events;
    5.  
    6. public class EventController : MonoBehaviour {
    7.     public class MouseClickHandler : UnityEvent<float, float> { };
    8.     MouseClickHandler mcHandler;
    9.     MySO someSO;
    10.  
    11.     private void Awake()
    12.     {
    13.         mcHandler = new MouseClickHandler();
    14.         someSO = ScriptableObject.CreateInstance<MySO>();
    15.     }
    16.  
    17.     void Start()
    18.     {
    19.         someSO.AddListener(OnSoEvent);
    20.     }
    21.     private void Update()
    22.     {
    23.         if (Input.GetMouseButtonDown(0))
    24.         {
    25.             mcHandler.Invoke(Input.mousePosition.x, Input.mousePosition.y);
    26.         }
    27.     }
    28.  
    29.     public void AddListener(UnityAction<float,float> method)
    30.     {
    31.         mcHandler.AddListener(method);
    32.     }
    33.  
    34.     public void OnSoEvent()
    35.     {
    36.         Debug.Log("I received an event from a scriptable Object");
    37.     }
    38.  
    39.  
    40. }
    41.  

    So I created a new project:
    • Added the Scriptable Object script
    • Created a GameObject called GameController
      • Tagged it GameController
      • Attached EventController Script to it.
    When i press play and click the mouse, the EventController sends out an event which the SO catches and prints:
    You pressed the mouse at XXX,YYY
    Then it Invokes its event and the GameController catches it and prints out
    I received and event from a scriptable object.

    So is there something different your trying to do from what I did above. Obviously its a simple case, but you can easily have Scriptable object subscribe to events, And you can SO's have events that MonoBehaviour, and other SOs subscribe too
     
    cranky likes this.
  3. JoshuaMcKenzie

    JoshuaMcKenzie

    Joined:
    Jun 20, 2015
    Posts:
    916
    I believe he tried to make a UnityEvent public on the ScriptableObject Asset and then tried to bind GameObjects to it through the inspector. That won't work. What important to get right when working with Scriptable Objects (when they are assets) is to know which side should know about the other.

    "->" means "...holds a reference to a..."
    ScriptableObject Asset -> ScriptableObject Asset = works fine. both are project-level assets and will have no issue referencing each other.
    ScriptableObject Asset -> Prefab = can work. both are project-level and Unity will let you bind and use these references, but often times it'll behave in the ways that you didn't intend. any changes to make through the SO to that prefab will change that prefab (and any new instances) not any pre-existing instances themselves.
    ScriptableObject Asset -> Scene Object = doesn't work. ScriptableObjects can't assume that it will only be used in that scene and if it were to be called in another scene you'd only get null references.
    Prefab -> ScriptableObject Asset = Works just fine. and once an instance is made from the prefab it to will have a proper reference.
    Scene Object -> ScriptableObject Asset = Works just fine. Scene-level data can access project-level data with no issues.

    as you can see you likely want to reverse how you're using the ScriptableObjects. Instead of giving your SOs events that your behaviours try to bind to, give them the event handlers and your monobehaviours will have the events, which then bind to the scriptableObjects public event handlers.

    I've done the same thing in my project as he did... make UnityEvents on my ScriptableObjects, but its accessed through public Add/remove/invoke functions. infact I have an entire folder full of scriptableobjects each of which has only one public UnityEvent (I use them as Global Events). But instead of binding to them in the inspector, I go to the GameObject and write a script that can hold a public reference to a ScriptableObjectEvent which it'll then bind itself OnEnable and unbind OnDisable (if it had a listener that needed to fire from the event) or to invoke the Event it through a function. For example I have a BoolEventData class with one instance named OnPauseEvent. five different scripts reference the instance to do their own logic, one toggles the Time.timesacle, another shows the pause screen, and a third changes the music. then I got another ten scripts also reference that OnPauseEvent and they'll fire the event when certain conditions happen (player input, controller disconnects, player idles too long, pause button on screen is pressed, whatever).
     
  4. cranky

    cranky

    Joined:
    Jun 11, 2014
    Posts:
    180
    You guys are close, but not quite. By object, I mean a .Net object, not a Unity Object.

    Picture my example above: a buff/debuff system. Certain abilities and effects apply a buff or a debuff to targets. All buff/debuffs are Buff ScriptableObjects, which specifies how long it should last, UI icon, name, description, etc. However, the functionality of the buff must be specified somehow as well, which is my problem. Maybe there's a buff that lowers a stat by 15%. It would need an OnApply() method in which it reduces that stat by 15% and an OnRemove() method to return the stat to normal. So I need a way to create these kinds of events so I can specify custom code for individual ScriptableObjects.

    Not sure if that makes much sense...
     
  5. takatok

    takatok

    Joined:
    Aug 18, 2016
    Posts:
    1,496
    So one thing that might help is understanding how your planning on organizing these things. Who is keeping track of the timer of these buffs. The buffs themselves, or some gameController? Mainly trying to figure out who needs to be the EventHandler and whose subscribing to the Events. It sounds like you want the ScriptableObject Debuff to subscribe to some EventHandler that is putting out OnApply, OnRemove events? And why would this EventHandler object be a .Net object and not a Unity Object?
     
    cranky likes this.
  6. cranky

    cranky

    Joined:
    Jun 11, 2014
    Posts:
    180
    It can all be flexible. Here's the idea in my head though (but it can change if necessary).

    The ScriptableObject is called BuffDefinition. It specifies basic information about the buff. It's just a container for information. Then when the game wants to apply a buff, it tells the BuffManager the BuffDefinition to apply, the target, and the source. The BuffManager creates a BuffInstance. The BuffInstance references the BuffDefinition for basic information such as name, starting time, etc. Ideally, BuffInstance would be an abstract class which overrides virtual methods such as OnApply and OnRemove.

    So all buffs would be classes which inherit from BuffInstance and contain code to control the behavior of the buff, in addition to the BuffDefinition (which supplies basic information that applies to all buffs). The base class, BuffInstance, provides the code to keep track of duration. So somehow I need to specify in the BuffDefinition which class of type BuffInstance the manager should use for the behavior.

    My current solution is to type the name of the type to use as a string, and at runtime, use reflection to get that type and create an instance of it. This isn't the most elegant or efficient method, but that's why I am here ;).

    Thank you for the help and patience, I hope I explained everything well.

    EDIT: For clarity, BuffInstance is NOT a Unity Object, aka MonoBehaviour, etc. The BuffManager is a MonoBehaviour attached to the player object and keeps the instances as an internal list.
     
  7. cranky

    cranky

    Joined:
    Jun 11, 2014
    Posts:
    180
    Any ideas?
     
  8. cranky

    cranky

    Joined:
    Jun 11, 2014
    Posts:
    180
    In case anyone else has a similar problem:

    I found success using: https://bitbucket.org/rotorz/classtypereference-for-unity

    There may be a better way to accomplish my goal, but this solution works. If anyone has a better idea, feel free to contribute. Thanks for all the help, guys!
     
  9. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    To be honest, I would stick to the idea that you described and apply the changes that @JoshuaMcKenzie has already mentioned.
    Can you precisely describe why it's not working for you?

    Plugin looks great though.
     
  10. JoshuaMcKenzie

    JoshuaMcKenzie

    Joined:
    Jun 20, 2015
    Posts:
    916
    Reflection is overkill for a system like this. its not needed at all. all thats really needed is a look into the design

    here is one way of going about it

    BuffDefinition: defines the stats AND behaviour of that are immuttable between all instances of the buff (things like what it does, how long it lasts, what can dispel it... data that would always be the same on all instances of that buff type) Buff definition is a ScriptableObject, so think of it as a blueprint for what the buff does in a general case.

    BuffInstance: the fact that its a Net object doesn't matter too much, just that since its not a Unity object you can't drag and drop it around in the inspector. but thats perfectly fine. BuffInstance should hold data that specific to that instance. things like if its Active, the Source, the Target and the current Duration are some key data points. also to simply all the trouble you're getting simply give the BuffInstance a Reference to the buff definition that it should use

    BuffManager: holds a list buff instances (or dictionary with a string as the key, so you could activate buffs by name, look into SerializableDictionaries) which on update will loop through the instances, find the ones that are active and send that instance to the referenced buffdefintion via a "BuffDefinition.Update(Buffinstance instance, float deltaTime)" and let the BuffDefinition perform the behaviour for that buffinstance

    BuffDefinition and BuffInstance are of course abstract so when you have a concrete type of buff, say "Wither" you'd follw Liskov's rule and pass the data as an abstract, and let the handler (buffdefinition) check if it can cast it.

    Code (CSharp):
    1. public interface IDamageReport
    2. {
    3.     public float DamageDealt{get;set;}
    4. }
    5. public interface IDamageable
    6. {
    7.     // applies damage to target, returns amount of damage actually dealt after resistances
    8.     public float Damage(float damageAmount);
    9. }
    10.  
    11. public interface IDamagePerTick
    12. {
    13.     public float Damage{get;}
    14.     public float Tick{get;}
    15.     public float TickTimer{get;set}
    16. }
    17.  
    18.  
    19. public class BuffInstance_Wither:BuffInstance,
    20.     IDamageReport, IDamagePerTick
    21. {
    22.     private float m_damageDealt =0;
    23.     private float m_tickTimer =0;
    24.  
    25.     [Tooltip("How much Damage is applied each tick")]
    26.     [SerializeField]private float m_damagePerTick =50f;
    27.     [Tooltip("How many seconds each tick is")]
    28.     [SerializeField]private float m_tick =0.5f;
    29.  
    30.     public float DamageDealt
    31.     {
    32.         get{return m_damageDealt;}
    33.         set{m_damageDealt = value;}
    34.     }
    35.  
    36.     public float Damage
    37.     {
    38.         get{return m_damagePerTick;}
    39.     }
    40.     public float Tick
    41.     {
    42.         get{return m_tick;}
    43.     }  
    44.     public float TickTimer
    45.     {
    46.         get{return m_tickTimer;}
    47.         set{m_tickTimer = value;}
    48.     }
    49.     public override void Reset()
    50.     {
    51.         base.Reset();
    52.         m_damageDealt = 0;
    53.         m_tickTimer = m_tick;
    54.     }
    55. }
    56.  
    57. public class BuffDefinition_Wither:BuffDefinition
    58. {
    59.  
    60.     public override void Update(BuffInstance instance, float deltaTime)
    61.     {
    62.         if(instance == null) return;
    63.         if(!instance.Target)// no target to wither, deactivate buff
    64.         {
    65.             instance.Active = false;
    66.             instance.Duration = 0;
    67.             return;
    68.         }
    69.  
    70.         base.Update(instance,deltaTime);
    71.        
    72.         IDamagePerTick ticker = instance as IDamagePerTick;
    73.        
    74.         if(ticker== null) return;
    75.  
    76.         ticker.TickTimer -= deltaTime;
    77.  
    78.         if(ticker.TickTimer>0) return;
    79.  
    80.         ticker.TickTimer = ticker.Tick;
    81.  
    82.         IDamageable damageableTarget = instance.Target.GetComponent<IDamageable>();
    83.         if(damageableTarget == null) return;
    84.  
    85.        float damageDealt = damageableTarget.Damage(ticker.Damage);
    86.  
    87.         IDamageReport damageReport = instance as IDamageReport;
    88.         if(damageReport == null) return;
    89.  
    90.         damageReport.DamageDealt += damageDealt;
    91.     }
    92.  
    93. }
    94.  
    95.  
    So Buff Manager will reset the state whenever the buff is activated, Buff Definition themselves will be fed the instance states and modify them, deactivating them when their time is up
     
    Last edited: Feb 27, 2017