When to Set Up Row Classes
With GHUD static data needs are met through the data tabs and asset references are met through the asset loader classes.
Row Classes can be used when behavior needs to be row specific or when a row specific value needs to be calculated at runtime.
They work well when certain rows need their own rules or effects.
The parent Row Class defines the common interface,
while individual Row Classes can override only the behavior that makes them different.
A good sign that Row Classes may be useful is code that repeatedly checks a row enum to decide what should happen:
switch (ability)
{
case EAbility.Slow:
// Special Slow behavior
break;
case EAbility.Fire0:
// Special Fire behavior
break;
}
With Row Classes, that behavior can instead live with the row it belongs to and be called through a common method.
Unique Row Effects
Row Classes are especially useful when a particular ability, spell, item, or other custom row type needs behavior that cannot be represented cleanly through ordinary data.
A virtual method can be defined on the parent Row Class, then overridden only by rows that need unique behavior.
For example, AbilitiesRow can define RunAbilityEffects(). Most abilities can use the default implementation, while SlowAbilitiesRow can override it to apply its unique slowing effect.
This keeps the special behavior with the row it belongs to instead of requiring the calling code to identify individual abilities with switch statements or other special-case checks.
Shared Behavior Among Multiple Rows
Sometimes several rows need the same behavior, but the behavior does not apply to every row in the tab.
In that case, add a shared helper method to the parent Row Class and call it only from the individual Row Classes that need it.
For example, several healing abilities could share an ApplyHeal method in the AbilitiesRow class:
// _AbilitiesRow.cs
protected void ApplyHeal(float amount, GHUDCharacter character = null, GHUDEnemy enemy = null)
{
if (character != null)
character.Heal(amount);
if (enemy != null)
enemy.Heal(amount);
}
Then each of the abilities that heal would call it from their override of the RunAbilitiyEffects method:
// [Healing]AbilitiesRow.cs files
public override void RunAbilityEffects(GHUDCharacter targetCharacter, GHUDEnemy targetEnemy)
{
ApplyHeal(Abilities.Power(Ability), targetCharacter, targetEnemy);
}
Any number of healing abilities can take advantage of ApplyHeal because it's available through their shared parent Row Class.
(For this example, imagine the Abilities tab uses a Power column to record the strength of each ability; one ability might use it for damage, while here we are showing how it might be used to indicate how much the healing abilities heal.)
Row Class Iteration
The Row Class structure also allows you to iterate over row-specific behavior, rather than only row data.
For example, you might want to determine which items are usable in a particular context, while allowing some items to have additional requirements.
For this situation, you could define a virtual method in ItemsRow, then override that method only in the child Item Row Classes that need special behavior.
// _ItemsRow.cs
public bool CanUse(ContextStruct contextStruct)
{
if(contextStruct.inItemLockedArea)
return false;
if(contextStruct.inventory[(int)Item] <= 0)
return false;
return CanUseByRow(contextStruct);
}
protected virtual bool CanUseByRow(ContextStruct contextStruct)
{
return true;
}
// HealthPotionItemsRow.cs
protected override bool CanUseByRow(ContextStruct contextStruct)
{
return contextStruct.currentHP < contextStruct.maxHP;
}
You can then iterate through all of the Row Classes and ask each item whether it can currently be used:
// From any class
List<EItem> usableItems = new();
foreach (ItemsRow row in Items.Rows)
{
if (row != null && row.CanUse(contextStruct))
usableItems.Add(row.Item);
}
Most items can use the default behavior, while only rows with additional requirements need their own implementation.
This allows iteration to account for row-specific behavior without building a large switch statement or other set of special-case checks outside of the Row Classes.
Unity Lifecycle Methods
Row Classes inherit from MonoBehaviour, and GHUD attaches each generated Row Class to a hidden GameObject when that tab's Row Classes are initialized. This means individual Row Classes can use Unity lifecycle methods such as Update().
This can be useful when behavior belongs to a particular row and needs to be evaluated continuously.
For example, a custom WorldEvents tab might contain an event that should activate whenever certain world conditions become true:
// MeteorShowerWorldEventsRow.cs
void Update()
{
if (WorldState.TimeOfDay >= 22f && !WorldState.MeteorShowerActive)
WorldState.StartMeteorShower();
}
Only the MeteorShowerWorldEventsRow needs this behavior. Other World Event rows do not need their own Update method.
If your tab needs to make use of the Update method in every row, you can also put Update in the parent Row Class and have it call a custom virtual or abstract OnUpdate method that individual child Row Classes can override.
Because every generated Row Class is its own MonoBehaviour, other Unity lifecycle methods can also be used when appropriate.
Keep in mind that there is one Row Class instance for each database row, not one instance for every object that uses that row. Row Classes are therefore best suited to row-level behavior rather than storing per-character, per-item, or other per-instance state; consider it valid for scenarios that would normally merit a singleton.
IMPORTANT: the hidden Row Class GameObject is created when the tab's Row Classes are first accessed through Rows or GetRow(). Lifecycle behavior will not begin before those Row Classes have been initialized.
If you need the Row Classes to begin running their lifecycle methods regardless of whether they are otherwise referenced, access [TabName].Rows once during game initialization.