Wayback Machinekoobas.hobune.stream
May JUN Jul
Previous capture 14 Next capture
2021 2022 2023
2 captures
13 Jun 22 - 14 Jun 22
sparklines
Close Help
  • Products
  • Solutions
  • Made with Unity
  • Learning
  • Support & Services
  • Community
  • Asset Store
  • Get Unity

UNITY ACCOUNT

You need a Unity Account to shop in the Online and Asset Stores, participate in the Unity Community and manage your license portfolio. Login Create account
  • Blog
  • Forums
  • Answers
  • Evangelists
  • User Groups
  • Beta Program
  • Advisory Panel

Navigation

  • Home
  • Products
  • Solutions
  • Made with Unity
  • Learning
  • Support & Services
  • Community
    • Blog
    • Forums
    • Answers
    • Evangelists
    • User Groups
    • Beta Program
    • Advisory Panel

Unity account

You need a Unity Account to shop in the Online and Asset Stores, participate in the Unity Community and manage your license portfolio. Login Create account

Language

  • Chinese
  • Spanish
  • Japanese
  • Korean
  • Portuguese
  • Ask a question
  • Spaces
    • Default
    • Help Room
    • META
    • Moderators
    • Topics
    • Questions
    • Users
    • Badges
  • Home /
avatar image
1
Question by Razputin · Dec 30, 2021 at 06:48 PM · scriptableobjecteffectdurationstatus

How to Keep Track of ScriptableObject Coroutine Durations?

I'm using scriptable objects to create my status effects.

The only issue I'm having is figuring out how to keep track of if the coroutine is already running.

I would like to be able to StopCoroutine on the status effects so I will need a reference to the Coroutine. I do not want to create a coroutine in my enemy script (PawnStats) for every possible status effect, and the coroutine cannot go in the ScriptableObject itself since it may be on multiple enemies at once.

I'm not sure how to proceed from here... any thoughts?

 public abstract class StatusEffect : ScriptableObject
 {
     public string statusEffectName = "New Status Effect Name";
     public string statusEffectDesc = "New Status Effect Desc";
  
     public abstract void OnApply(PawnStats pawnStats);
     public abstract IEnumerator OnEffect(PawnStats pawnStats);
     public abstract void OnRemove(PawnStats pawnStats);
 }

The StatusEffect that will have a duration of some sort.

 public class StatusEffectTimed : StatusEffect
 {
  
     public float duration = 3;
  
     public override void OnApply(PawnStats pawnStats)
     {
         pawnStats.AddStatusEffect(this);
     }
  
     public override IEnumerator OnEffect(PawnStats pawnStats)
     {
         //Modify stats here probably. (I.E. Make enemy walk 1/2 speed)
         yield return new WaitForSeconds(duration);
         //Revert modifications back to default (I.E. Set enemy back to default Speed)
     }
  
     public override void OnRemove(PawnStats pawnStats)
     {
         pawnStats.RemoveStatusEffect(this);
     }
 }

The Code on my enemy script (PawnStats) that's being called above. This is where the confusion lies.

 public List<StatusEffect> statusEffectList = new List<StatusEffect>();
 public void AddStatusEffect(StatusEffect statusEffect)
 {
     statusEffectList.Add(statusEffect);
     Coroutine co = StartCoroutine(statusEffect.OnEffect(this)); //This is the part I'm not sure how to impliment in a way to keep track of the coroutine effectively.
 }
  
 public void RemoveStatusEffect(StatusEffect statusEffect)
 {
     statusEffectList.Remove(statusEffect);
 }

 







Comment
Add comment
10 |3000 characters needed characters left characters exceeded
▼
  • Viewable by all users
  • Viewable by moderators
  • Viewable by moderators and the original poster
  • Advanced visibility
Viewable by all users

1 Reply

· Add your reply
  • Sort: 
avatar image
1
Best Answer

Answer by andrew-lukasik · Jan 01 at 02:20 AM

My suggestion is to refactor this from Coroutines to a tiny bit more data-oriented, where your StatusEffect becomes a logic-less struct and handle logic somewhere else entirely (stat effect visualization system, for example). alt text alt text

PawnPersistentData.cs

 using System.Collections.Generic;
 using System.Linq;
 using UnityEngine;
 
 public class PawnPersistentData : MonoBehaviour, ISerializationCallbackReceiver
 {
     Dictionary<EPawnStat,float> Stats = new Dictionary<EPawnStat,float>(){
         { EPawnStat.Life , 1.0f } ,
         { EPawnStat.LifeMax , 1.0f } ,
         { EPawnStat.Stamina , 1.0f } ,
         { EPawnStat.StaminaMax , 1.0f } ,
         { EPawnStat.WalkSpeed , 6.0f } ,
         { EPawnStat.WalkSpeedMax , 6.0f } ,
     };
     [SerializeField] StatsKV[] _Stats;// serialized
     [SerializeField] List<StatsKV> StatsFinalPreview = new List<StatsKV>();// Read-only, editor-only preview so you can look in up in the inspector window
     [SerializeField] Dictionary<string,StatEffect> TemporalStatEffects = new Dictionary<string,StatEffect>();
     [SerializeField] TemporalStatEffectsKV[] _TemporalStatEffects;// serialized
 
 #region TEST
     [SerializeField] StatEffectObject _test;
     void OnEnable () => AddStatEffects( _test );
     void OnDisable () => RemoveStatEffects( _test );
     [ContextMenu("TEST/hit")] void _TestHit () => ImmediateStatChange( EPawnStat.Life , StatEffect.EOperator.Add , -0.1f );
     [ContextMenu("TEST/heal")] void _TestHeal () => ImmediateStatChange( EPawnStat.Life , StatEffect.EOperator.Add , 1f );
     [ContextMenu("TEST/arrow to the knee")] void _TestArrowToTheKnee () => ImmediateStatChange( EPawnStat.WalkSpeedMax , StatEffect.EOperator.Multiply , 0.5f );
 #endregion
 
     void FixedUpdate ()
     {
         float dt = Time.fixedDeltaTime;
         for( int i=TemporalStatEffects.Count-1 ; i!=-1 ; i-- )
         {
             var kv = TemporalStatEffects.ElementAt(i);
             var key = kv.Key;
             var effect = TemporalStatEffects[key];
             effect.Seconds -= dt;
             if( effect.Seconds>0 ) TemporalStatEffects[key] = effect;
             else TemporalStatEffects.Remove(key);
         }
     }
 
     public float GetValue ( EPawnStat stat )
     {
         float finalValue = Stats[stat];
         foreach( var kv in TemporalStatEffects )
         {
             var effect = kv.Value;
             if( effect.Stat==stat )
             {
                 if( effect.Operator==StatEffect.EOperator.Add )
                     finalValue += effect.Value;
                 else if( effect.Operator==StatEffect.EOperator.Multiply )
                     finalValue *= effect.Value;
                 else Debug.LogError($"{nameof(StatEffect.EOperator)} {effect.Operator} not implemented here");
             }
         }
         return finalValue;
     }
 
     public void ImmediateStatChange ( EPawnStat stat , StatEffect.EOperator op , float value )
     {
         if( op==StatEffect.EOperator.Add )
             Stats[stat] += value;
         else if( op==StatEffect.EOperator.Multiply )
             Stats[stat] *= value;
         else Debug.LogError($"{nameof(StatEffect.EOperator)} {op} not implemented here");
         
         ValidateStats();
     }
 
     public void AddStatEffects ( StatEffectObject def )
     {
         foreach( var next in def.GetStatEffects() )
             TemporalStatEffects.Add( next.Guid , next );
     }
     public void RemoveStatEffects ( StatEffectObject def )
     {
         foreach( var next in def.GetStatEffects() )
             TemporalStatEffects.Remove( next.Guid );
     }
 
     public void ValidateStats ()
     {
         Stats[EPawnStat.Life] = Mathf.Clamp( Stats[EPawnStat.Life] , 0 , Stats[EPawnStat.LifeMax] );
         Stats[EPawnStat.Stamina] = Mathf.Clamp( Stats[EPawnStat.Stamina] , 0 , Stats[EPawnStat.StaminaMax] );
         Stats[EPawnStat.WalkSpeed] = Mathf.Clamp( Stats[EPawnStat.WalkSpeed] , 0 , Stats[EPawnStat.WalkSpeedMax] );
     }
 
 #region ISerializationCallbackReceiver
     void ISerializationCallbackReceiver.OnAfterDeserialize ()
     {
         Stats.Clear();
         if( _Stats!=null )
         {
             foreach( var kv in _Stats )
                 Stats.Add( kv.Key , kv.Value );
         }
 
         TemporalStatEffects.Clear();
         if( _TemporalStatEffects!=null )
         {
             foreach( var kv in _TemporalStatEffects )
                 TemporalStatEffects.Add( kv.Key , kv.Value );
         }
     }
     void ISerializationCallbackReceiver.OnBeforeSerialize ()
     {
         ValidateStats();
         _Stats = new StatsKV[ Stats.Count ];
         {
             int i = 0;
             foreach( var kv in Stats )
                 _Stats[ i++ ] = new StatsKV{ Key=kv.Key , Value=kv.Value };
         }
 
         _TemporalStatEffects = new TemporalStatEffectsKV[ TemporalStatEffects.Count ];
         {
             int i = 0;
             foreach( var kv in TemporalStatEffects )
                 _TemporalStatEffects[ i++ ] = new TemporalStatEffectsKV{ Key=kv.Key , Value=kv.Value };
         }
 
         StatsFinalPreview.Clear();
         foreach( var kv in Stats )
             StatsFinalPreview.Add( new StatsKV{ Key=kv.Key , Value=GetValue(kv.Key) } );
     }
     [System.Serializable] public struct StatsKV { public EPawnStat Key; public float Value; }
     [System.Serializable] public struct TemporalStatEffectsKV { public string Key; public StatEffect Value; }
 #endregion
 
 }

StatEffectObject.cs

 using System.Collections.Generic;
 using UnityEngine;
 
 [CreateAssetMenu( menuName="PROJECT/Stat Effect Definition" , fileName="stat effect definition 0" )]
 public class StatEffectObject : ScriptableObject
 {
     [SerializeField] StatEffect[] _statEffects;
     public IEnumerable<StatEffect> GetStatEffects () => _statEffects;
     #if UNITY_EDITOR
     void OnValidate ()
     {
         for( int i=_statEffects.Length-1 ; i!=-1 ; i-- )
             if( string.IsNullOrEmpty(_statEffects[i].Guid) )
                 _statEffects[i].Guid = System.Guid.NewGuid().ToString();
     }
     #endif
 }
 
 [System.Serializable]
 public struct StatEffect
 {
     public string Guid;
     public EPawnStat Stat;
     public float Value;
     public EOperator Operator;
     public float Seconds;
     public enum EOperator : byte { Add , Multiply }
 }
 
 public enum EPawnStat : uint { UNDEFINED = 0 , Life , LifeMax , Stamina , StaminaMax , WalkSpeed , WalkSpeedMax }
 

screenshot-2022-01-01-031132.jpg (40.1 kB)
screenshot-2022-01-01-030815.jpg (76.9 kB)
Comment
Add comment · Show 3 · Share
10 |3000 characters needed characters left characters exceeded
▼
  • Viewable by all users
  • Viewable by moderators
  • Viewable by moderators and the original poster
  • Advanced visibility
Viewable by all users
avatar image Razputin · Jan 01 at 07:17 AM 0
Share

Thank you for the extremely detailed answer, it's a bit more complex than I'm used to but I'm confident I'll be able to grasp it after studying it a bit. Thanks so much!

avatar image andrew-lukasik Razputin · Jan 01 at 12:17 PM 0
Share

It looks complex only because damn Unity still won't let serialize/inspect Dictionary so portion of this PawnPersistentData.cs is just working around this limitation.

Core idea is simple tho: trade Coroutine for a struct and iterate over those structs to update their values (time left, etc.). Reason: It's complicated to track/serialize a Coroutine's state where as a struct is simple to serialize and maintain.

avatar image andrew-lukasik Razputin · Jan 01 at 12:27 PM 0
Share

Dictionary<EPawnStat,float> Stats holds base stat values (without temporal stat effects taken into account).

public float GetValue ( EPawnStat stat ) calculates the final value of a stat (with stat effects applied)

 float pawnLifeNow = pawnPersistentData.GetValue ( EPawnStat.Life );
 float pawnLifeMax = pawnPersistentData.GetValue ( EPawnStat.LifeMax );

Guid (unique names, essantially) are there to allow identification of those effects, to know which is what. You can name them in human-readable form ("sta$$anonymous$$a damage" for example).

Your answer

Hint: You can notify a user about this post by typing @username

Up to 2 attachments (including images) can be used with a maximum of 524.3 kB each and 1.0 MB total.

Follow this Question

Answers Answers and Comments

137 People are following this question.

avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image

Related Questions

Card Game, cards as ScriptableObject, card effects as ScriptableObjects, effects have different parameters. How to make an inspector UI support this structure? 2 Answers

Buff/debuff system (Temporarily change variables) 1 Answer

How to attach attack effect scripts to the attack scriptableobject in a turn based battler. 0 Answers

FSM State implementation — SO, SO+wrappers, custom classes? 0 Answers

Show multiple attached scripts variables in one script in the inspector 0 Answers


Enterprise
Social Q&A

Social
Subscribe on YouTube social-youtube Follow on LinkedIn social-linkedin Follow on Twitter social-twitter Follow on Facebook social-facebook Follow on Instagram social-instagram

Footer

  • Purchase
    • Products
    • Subscription
    • Asset Store
    • Unity Gear
    • Resellers
  • Education
    • Students
    • Educators
    • Certification
    • Learn
    • Center of Excellence
  • Download
    • Unity
    • Beta Program
  • Unity Labs
    • Labs
    • Publications
  • Resources
    • Learn platform
    • Community
    • Documentation
    • Unity QA
    • FAQ
    • Services Status
    • Connect
  • About Unity
    • About Us
    • Blog
    • Events
    • Careers
    • Contact
    • Press
    • Partners
    • Affiliates
    • Security
Copyright © 2020 Unity Technologies
  • Legal
  • Privacy Policy
  • Cookies
  • Do Not Sell My Personal Information
  • Cookies Settings
"Unity", Unity logos, and other Unity trademarks are trademarks or registered trademarks of Unity Technologies or its affiliates in the U.S. and elsewhere (more info here). Other names or brands are trademarks of their respective owners.
  • Anonymous
  • Sign in
  • Create
  • Ask a question
  • Spaces
  • Default
  • Help Room
  • META
  • Moderators
  • Explore
  • Topics
  • Questions
  • Users
  • Badges