- Home /
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);
}
Answer by andrew-lukasik · Jan 01 at 02:20 AM
My suggestion is to refactor this from Coroutine
s 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).
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 }
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!
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.
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
Follow this Question
Related Questions
How to attach attack effect scripts to the attack scriptableobject in a turn based battler. 0 Answers
Buff/debuff system (Temporarily change variables) 1 Answer
How to prevent an inherited variable from showing up on a scriptable object? 1 Answer
Unity recompilation time slowly increases each time it's recompiled. Why? 0 Answers