Hunger and Starvation

Hello,

I am currently trying to get a Starving script working. I set up my 4 primary attributes, Health/Stamina/Hunger/Thirst. These were quite easy to do in the attribute Manager.

However, I am trying to put together a Script so that when Hunger and Thirst hit 0, it begins to damage the players health over time. Below is the code I am currently using. I thought I could test it with doing a damage of 50 health since I have 100 to start with, which would have been obvious if it was working as intended or not. Using health.Damage(50) killed me in 1 hit though. I reduced it to 10 and even 1 as you see in the code and still it kills me in 1 hit. I want starving to be bad, but not instant death.

I was hoping someone knows where I went wrong and also how I could convert it to deal damage over time once my Hunger hits 0. I also suppose I will have to add a couple more things to make the code neater and stop killing me after I am dead as it seems to want to do now. I can probably just have a start and stop function.

I appreciate any help, thanks ahead of time! (I assume that for the dehydration script I can duplicate this and rename from starving to dehydration then simply change the attribute it looks for from hunger to Thirst and adjust the Damage over Time as needed.)

C#:
using Opsive.Shared.Events;
using Opsive.UltimateCharacterController.Traits;
using UnityEngine;


public class Starving : MonoBehaviour
{
    [Tooltip("The character that has the health component.")]
    [SerializeField] protected GameObject m_Character;

    [Tooltip("The AttributeManager that contains the attribute. If null the character's Attribute Manager will be used.")]
    [SerializeField] protected AttributeManager m_AttributeManager;

    [Tooltip("The name of the attribute that the UI should monitor.")]
    [SerializeField] protected string m_AttributeName = "Hunger";

    private Attribute m_Attribute;

    public void Awake()
    {
        if (m_AttributeManager != null)
        {
            EventHandler.RegisterEvent<Attribute>(m_AttributeManager.gameObject, "OnAttributeUpdateValue", OnUpdateValue);
        }
    }

    private void OnUpdateValue(Attribute attribute)
    {
        m_Attribute = m_AttributeManager.GetAttribute(m_AttributeName);
        Debug.Log(m_AttributeName + "Value:" + m_Attribute.Value);
        if (m_Attribute.Value <= 0)
        {
            Debug.Log("You Are Starving");
            var health = m_Character.GetComponent<Health>();
            health.Damage(1);
        }
    }

    public void OnDestroy()
    {
        if (m_AttributeManager != null)
        {
            EventHandler.UnregisterEvent<Attribute>(m_AttributeManager.gameObject, "OnAttributeUpdateValue", OnUpdateValue);
        }
    }
}
 
Last edited:
I think the easiest approach would be to start a coroutine once the attribute reaches 0, which would damage the character every X seconds, e.g.:

C#:
using System.Collections;

...
    
Coroutine damageCoroutine;

...
    
// when attribute reaches 0
damageCoroutine = StartCoroutine(PeriodicDamage);

...

IEnumerator PeriodicDamage() {
    float t = 0;
    while (true) {
        t += Time.deltaTime;
        if (t >= 1) {
            health.Damage(1);
            t -= 1;
        }
        yield return null;
    }
}

...

void OnDestroy() {
    if (damageCoroutine != null) StopCoroutine(damageCoroutine);
    ...
}
 
This almost worked except it only did 1 damage to health on the first tick then it gets increasingly larger each tick. I also noticed that the only attribute restored on death is Health. The attributes I added, Stamina, Hunger, Thirst, all stay at 0 after death. Due to the PeriodicDamage increasing exponentially, after the first death I get 1 shot each time because it causes over 100 damage.

Even if I use the inspector to manually restore hunger to 100 on respawn, it seems to ignore it and still kills me in 1 hit.
 
So I updated the code to use the Attribute Managers instead of a coroutine. Once my hunger hits 0, it correctly damages my health 1 damage per second. Also, if I restore Hunger in the inspector, it stops the health drain. When hunger again reaches 0, the drain continues. Only problem.. I do not seem to die when health hits 0.

Code:
using Opsive.Shared.Events;
using Opsive.UltimateCharacterController.Traits;
using UnityEngine;

public class Starving : MonoBehaviour
{
    [Tooltip("The character that has the health component.")]
    [SerializeField] protected GameObject m_Character;

    [Tooltip("The AttributeManager that contains the attribute. If null the character's Attribute Manager will be used.")]
    [SerializeField] protected AttributeManager m_AttributeManager;

    [Tooltip("The name of the attribute that the UI should monitor.")]
    [SerializeField] protected string m_AttributeName = "Hunger";

    private Attribute m_Attribute;
    private Health m_Health;

    public void Awake()
    {
        if (m_AttributeManager != null)
        {
            EventHandler.RegisterEvent<Attribute>(m_AttributeManager.gameObject, "OnAttributeUpdateValue", OnUpdateValue);
        }
    }

    private void OnUpdateValue(Attribute attribute)
    {
        m_Attribute = m_AttributeManager.GetAttribute(m_AttributeName);
        Debug.Log(m_AttributeName + "Value:" + m_Attribute.Value);
        if (m_Attribute.Value != m_Attribute.MinValue)
        {
            m_AttributeManager.GetAttribute("Health").CancelAutoUpdate();
            m_Attribute.AutoUpdateValueType = Attribute.AutoUpdateValue.Decrease;
            return;
        }
      
            Debug.Log("You Are Starving");

            // when Hunger reaches 0
            m_AttributeManager.GetAttribute("Health").AutoUpdateValueType = Attribute.AutoUpdateValue.Decrease;
            m_AttributeManager.GetAttribute("Health").AutoUpdateAmount = 1;
            m_AttributeManager.GetAttribute("Health").AutoUpdateInterval = 1;

    }

    private void OnDestroy()
    {
        if (m_AttributeManager != null)
        {
            EventHandler.UnregisterEvent<Attribute>(m_AttributeManager.gameObject, "OnAttributeUpdateValue", OnUpdateValue);
        }
    }
}

p.s. I will clean up the code once it is working how I want it to. For now I was more annoyed and not caring heh.
 
Last edited:
I have converted my script to being an ability. Admittedly I tried to copy the drowning skip a tad...

However I run into 1 of 2 issues.

Issue #1)
if (m_Health != null)
{
m_Health.ImmediateDeath();
}

If this is not commented out, I die as soon as Hunger hits 0. If this IS commented out, then when my hunger hits 0, my health correctly starts draining to 0. If I restore hunger, health stop draining and continues again once hunger reaches 0...but..if Health reaches 0.. I do not die.

I will keep tinkering, but I will paste my code below if anyone has a solution.

C#:
using Opsive.Shared.Events;
using Opsive.Shared.Game;
using Opsive.UltimateCharacterController.Character.Abilities;
using Opsive.UltimateCharacterController.Traits;
using UnityEngine;

/// <summary>
/// Plays a starving animation when the character is out of hunger.
/// </summary>
[DefaultStartType(AbilityStartType.Manual)]
[DefaultAbilityIndex(304)] // Change this to a starving animation TO-DO
[DefaultAllowPositionalInput(false)]
[DefaultAllowRotationalInput(false)]
[DefaultUseGravity(AbilityBoolOverride.False)]
[DefaultEquippedSlots(0)]
public class Starving : Ability
{

    private Attribute m_Attribute;
    private Attribute m_HealthAttribute;
    private Attribute.AutoUpdateValue m_AutoUpdateValue;
    private Health m_Health;
    private Respawner m_Respawner;

    public override bool CanStayActivatedOnDeath { get { return true; } }

    /// <summary>
    /// Initialize the default values.
    /// </summary>
    public override void Awake()
    {
        base.Awake();

        var attributeManager = m_GameObject.GetCachedComponent<AttributeManager>();
        if (attributeManager == null)
        {
            Debug.LogError("Error: The character must have an Attribute Manager.");
            return;
        }

        m_Attribute = attributeManager.GetAttribute("Hunger");
        if (m_Attribute == null)
        {
            Debug.LogError("Error: Unable to find the attribute with name " + m_Attribute + ".");
            return;
        }
        if (attributeManager != null)
        {
            EventHandler.RegisterEvent<Attribute>(attributeManager.gameObject, "OnAttributeUpdateValue", OnUpdateValue);
        }

        m_Health = m_GameObject.GetCachedComponent<Health>();
        m_Respawner = m_GameObject.GetCachedComponent<Respawner>();
        m_HealthAttribute = attributeManager.GetAttribute("Health");

        EventHandler.RegisterEvent(m_Attribute, "OnAttributeReachedDestinationValue", Starve);
        EventHandler.RegisterEvent(m_GameObject, "OnAnimatorDrownComplete", OnStarveComplete);
    }


    private void OnUpdateValue(Attribute attribute)
    {
        if (m_Attribute.Value != m_Attribute.MinValue)
        {
            StarveCanceled();
        }
        return;
    }

    /// <summary>
    /// The character Starving.
    /// </summary>
    private void Starve()
    {
        if (m_Attribute.Value != m_Attribute.MinValue)
        {
            return;
        }
        Debug.Log("You Are Starving");
        StartAbility();
    }

    /// <summary>
    /// The ability has started.
    /// </summary>
    protected override void AbilityStarted()
    {
        base.AbilityStarted();

        m_HealthAttribute.AutoUpdateValueType = Attribute.AutoUpdateValue.Decrease;
        m_HealthAttribute.AutoUpdateAmount = 10;
        m_HealthAttribute.AutoUpdateInterval = 1;

        if (m_Health != null)
        {
            m_Health.ImmediateDeath();
        }
    }

    /// <summary>
    /// The character has finished drowning.
    /// </summary>
    private void OnStarveComplete()
    {
        if (IsActive && m_Respawner != null)
        {
            m_Respawner.Respawn();
        }

        StopAbility();
    }

    private void StarveCanceled()
    {
        if (m_Attribute.Value > m_Attribute.MinValue)
        {
            m_HealthAttribute.AutoUpdateValueType = Attribute.AutoUpdateValue.None;
            m_Attribute.AutoUpdateValueType = Attribute.AutoUpdateValue.Decrease;
        }
 
        StopAbility();
    }

    /// <summary>
    /// The ability has stopped running.
    /// </summary>
    /// <param name="force">Was the ability force stopped?</param>
    protected override void AbilityStopped(bool force)
    {
        base.AbilityStopped(force);

        m_Attribute.ResetValue();
        m_Attribute.AutoUpdateValueType = m_AutoUpdateValue;
    }

    /// <summary>
    /// The character has been destroyed.
    /// </summary>
    public override void OnDestroy()
    {
        base.OnDestroy();

        if (m_Attribute != null)
        {
            EventHandler.UnregisterEvent(m_Attribute, "OnAttributeReachedDestinationValue", Starve);
            EventHandler.UnregisterEvent(m_GameObject, "OnAnimatorDrownComplete", OnStarveComplete);
        }
    }
}
 
So I cleaned up the code a tad, though there is probably a more efficient way to do it.

What Works:
1) Hunger Attribute drains to 0
2) At 0 Hunger, Health starts to drain.
3) If Hunger is restored above 0, Health stop draining and continues once it falls back to 0.

What does not:
I cannot seem to figure out how to die when the Health attribute hits 0. I have tried multiple ways and can only seem to get a kill command to work when Hunger, not health, hits 0.


C#:
using Opsive.Shared.Events;
using Opsive.Shared.Game;
using Opsive.UltimateCharacterController.Character.Abilities;
using Opsive.UltimateCharacterController.Traits;
using UnityEngine;

/// <summary>
/// Plays a starving animation when the character is out of hunger.
/// </summary>
[DefaultStartType(AbilityStartType.Manual)]
[DefaultAbilityIndex(304)] // Change this to a starving animation TO-DO
[DefaultAllowPositionalInput(false)]
[DefaultAllowRotationalInput(false)]
[DefaultUseGravity(AbilityBoolOverride.False)]
[DefaultEquippedSlots(0)]
public class Starving : Ability
{
    private Attribute m_Attribute;
    private Attribute m_HealthAttribute;
    private Attribute.AutoUpdateValue m_AutoUpdateValue;
    private Health m_Health;
    private Respawner m_Respawner;

    public override bool CanStayActivatedOnDeath { get { return true; } }

    /// <summary>
    /// Initialize the default values.
    /// </summary>
    public override void Awake()
    {
        base.Awake();

        var attributeManager = m_GameObject.GetCachedComponent<AttributeManager>();

        m_Attribute = attributeManager.GetAttribute("Hunger");
        m_Health = m_GameObject.GetCachedComponent<Health>();
        m_Respawner = m_GameObject.GetCachedComponent<Respawner>();
        m_HealthAttribute = attributeManager.GetAttribute("Health");

        if (attributeManager == null)
        {
            Debug.LogError("Error: The character must have an Attribute Manager.");
            return;
        }
        if (m_Attribute == null)
        {
            Debug.LogError("Error: Unable to find the attribute with name " + m_Attribute + ".");
            return;
        }
        if (attributeManager != null)
        {
            EventHandler.RegisterEvent<Attribute>(attributeManager.gameObject, "OnAttributeUpdateValue", OnUpdateValue);
        }

        EventHandler.RegisterEvent(m_Attribute, "OnAttributeReachedDestinationValue", Starve);
        EventHandler.RegisterEvent(m_GameObject, "OnAnimatorDrownComplete", OnStarveComplete);
    }
    /// <summary>
    /// Checks Attribute Value Updates
    /// If Hunger goes back above 0, Stop Starving
    /// If Health hits 0, you SHOULD Die.
    /// </summary>
    private void OnUpdateValue(Attribute attribute)
    {
        if (m_Attribute.Value != m_Attribute.MinValue)
        {
            StarveCanceled();
        }
        else if (m_Health.Value == 0)
        {
            Debug.Log("You Have Died!");
            //This is giving a Debug saying the character died, but the following does not actually kill the character.
            m_Health.ImmediateDeath();
        }
        return;
    }
    /// <summary>
    /// The character Starving.
    /// </summary>
    private void Starve()
    {
        if (m_Attribute.Value != m_Attribute.MinValue)
        {
            return;
        }
        StartAbility();
    }
    /// <summary>
    /// The ability has started.
    /// </summary>
    protected override void AbilityStarted()
    {
        base.AbilityStarted();

        Debug.Log("You Are Starving");

        //Drains Health Attribute when ability is Active.
        m_HealthAttribute.AutoUpdateValueType = Attribute.AutoUpdateValue.Decrease;
        m_HealthAttribute.AutoUpdateAmount = 10;
        m_HealthAttribute.AutoUpdateInterval = 1;
    }
    /// <summary>
    /// The Character has starved to Death; Stop Starve Ability
    /// </summary>
    private void OnStarveComplete()
    {
        if (IsActive && m_Respawner != null)
        {
            m_Respawner.Respawn();
        }
        StopAbility();
    }
    /// <summary>
    /// If Hunger goes back above 0, Stop Starving
    /// </summary>
    private void StarveCanceled()
    {
        if (m_Attribute.Value > m_Attribute.MinValue)
        {
            m_HealthAttribute.AutoUpdateValueType = Attribute.AutoUpdateValue.None;
            m_Attribute.AutoUpdateValueType = Attribute.AutoUpdateValue.Decrease;
        }
        StopAbility();
    }
    /// <summary>
    /// The ability has stopped running.
    /// </summary>
    /// <param name="force">Was the ability force stopped?</param>
    protected override void AbilityStopped(bool force)
    {
        base.AbilityStopped(force);

        m_Attribute.ResetValue();
        m_Attribute.AutoUpdateValueType = m_AutoUpdateValue;
    }
    /// <summary>
    /// The character has been destroyed.
    /// </summary>
    public override void OnDestroy()
    {
        base.OnDestroy();

        if (m_Attribute != null)
        {
            var attributeManager = m_GameObject.GetCachedComponent<AttributeManager>();
            EventHandler.UnregisterEvent(m_Attribute, "OnAttributeReachedDestinationValue", Starve);
            EventHandler.UnregisterEvent(m_GameObject, "OnAnimatorDrownComplete", OnStarveComplete);
            EventHandler.UnregisterEvent<Attribute>(attributeManager.gameObject, "OnAttributeUpdateValue", OnUpdateValue);
        }
    }
}
 
All ImmediateDeath really does is call Health.Damage using the current amount of health/shield. I wonder if calling it when the Health value is already 0 therefore does nothing useful. Try calling Health.Damage(999) instead and see what happens.
 
So I tried changing the min value of Health to 1 and it still had no effect.
I have tried both of these ways. I get the Debug when health hits 0(or 1 when I tried that), but not the death.1618427455591.png

I am just not understanding this, I thought this would be a simple script to pull off haha.
 
Hm are you using the CharacterHealth component with the correct Health Attribute selected and Invincible off? Any states affecting that component? Is the Death event being called at all? (You could test this by logging in the Health.Die method).
 
1) Yes I tried both Health and Character Health. It is also not set to invincible.
[SerializeField] protected Health m_Health;
[SerializeField] protected CharacterHealth m_Health;

1618479446423.png

2) I do not have any states set up for attributes.

3) Honestly I just tried to follow the drown script so I did not call the Death event. What I am finding frustrating is m_Health.ImmediateDeath(); will kill me, just not when I want it to. For example( I re-wrote this again and took it out of the abilities to make sure I had as little outside interference as possible, so please forgive the hasty bit of code)

But looking at this code. If I put m_Health.ImmediateDeath(); under the Debug for Debug.Log("You Are Starving"); The Character will die when Hunger hits 0.

However, if I put m_Health.ImmediateDeath(); under the Debug for Debug.Log("You Have Died!"); I DO get the Debug letting me know I died(because my Health hit 0, or 1 when I tested that as a minimum) but the character will not die.


C#:
using Opsive.Shared.Events;
using Opsive.UltimateCharacterController.Traits;
using UnityEngine;


public class StarvingTest : MonoBehaviour
{
    [Tooltip("The character that has the health component.")]
    [SerializeField] protected GameObject m_Character;

    [Tooltip("The AttributeManager that contains the attribute. If null the character's Attribute Manager will be used.")]
    [SerializeField] protected AttributeManager m_AttributeManager;

    [Tooltip("The AttributeManager that contains the attribute. If null the character's Attribute Manager will be used.")]
    [SerializeField] protected Health m_Health;

    private Attribute m_HungerAttribute;
    private Attribute m_HealthAttribute;



    private void Awake()
    {
        m_HealthAttribute = m_AttributeManager.GetAttribute("Health");
        m_HungerAttribute = m_AttributeManager.GetAttribute("Hunger");
        //m_Health = m_Character.GetComponent<Health>();

        if (m_AttributeManager != null)
        {
            EventHandler.RegisterEvent<Attribute>(m_AttributeManager.gameObject, "OnAttributeUpdateValue", OnUpdateValue);
        }
    }
    /// <summary>
    /// Checks Attribute Value Updates
    /// If Hunger Attribute reaches minimum Value, You are starving.
    /// If Hunger goes back above 0, You Stop Starving.
    /// If Health hits 0, you SHOULD Die.
    /// </summary>
    private void OnUpdateValue(Attribute attribute)
    {
        if (m_HungerAttribute.Value == m_HungerAttribute.MinValue)
        {
            Debug.Log("You Are Starving");

            //Drains Health Attribute when ability is Active.
            m_HealthAttribute.AutoUpdateValueType = Attribute.AutoUpdateValue.Decrease;
            m_HealthAttribute.AutoUpdateAmount = 10;
            m_HealthAttribute.AutoUpdateInterval = 1;
        }
        else if (m_HealthAttribute.Value <= m_HealthAttribute.MinValue)
        {
            Debug.Log("You Have Died!");
            //This is giving a Debug saying the character died, but the following does not actually kill the character.
            m_Health.ImmediateDeath();
            m_Health.Damage(999);
            
        }
        StarveCanceled();
        return;
    }
    /// <summary>
    /// If Hunger goes back above 0, Stop Starving
    /// </summary>
    private void StarveCanceled()
    {
        if (m_HungerAttribute.Value > m_HungerAttribute.MinValue)
        {
            m_HealthAttribute.AutoUpdateValueType = Attribute.AutoUpdateValue.None;
            m_HungerAttribute.AutoUpdateValueType = Attribute.AutoUpdateValue.Decrease;
        }
    }
    /// <summary>
    /// The character has been destroyed.
    /// </summary>
    private void OnDestroy()
    {
        if (m_HungerAttribute != null && m_HealthAttribute !=null)
        {
            EventHandler.UnregisterEvent<Attribute>(m_AttributeManager.gameObject, "OnAttributeUpdateValue", OnUpdateValue);
        }
    }
}



Is it possible I just need to write this in 2 split parts? It seems to have trouble watching 2 attributes in 1 script.
 
So I made a 2nd script just to watch the HP value to test it and found something very odd.



private void OnUpdateValue(Attribute attribute)
{
if (m_HealthAttribute.Value <= m_HealthAttribute.MinValue)
{
Debug.Log("You Have Died From Starving!");
//This is giving a Debug saying the character died, but the following does not actually kill the character.
m_Health.ImmediateDeath();
m_Health.Damage(999);
}
return;
}




^ This is what I was using just to monitor the Attribute. Now if I did Value != MinValue, I would die the second I hit play. So it is able to kill me. However anything else it does not kill me. whether it be Value == MinValue, Value <= MinValue, or even Value <= 0.



BUT THEN I FOUND A CLUE If I did Value <= ( All these Values 90,80,70,60,50,40,30,20,10) it would kill me. However anything 9/8/7/6/5/4/3/2/1/0 Will not kill me. I think I found the issue. I believe it is with the auto update will let you know soon
 
Okay so I tried turning off the update value once Health hit 0 thinking maybe wires were getting crossed between that attempting to update and the attribute update value check but it did not affect anything. I guess I will try reducing the damage to see just how low I can effectively die at.
 
I now have it set up so that I Starve at 1 damage to my health per second. And When I get to m_Health.Value <= 1 It will kill me. but if I put m_Health.Value <=0 or m_Health == 0 it will not kill me.

So is this a bug then?
 
If you've got the Health attribute auto-updating as you said then there could indeed be a conflict there. You should disable its auto-update first. Or you could instead set this with a couple of abilities and states to have each attribute auto-decrease at the right time, though this would probably require a bit of tricky state setup with state blocking.
 
I mean, I changed it so that the auto update stopped when health was 0 and it still would not kill me at 0. The lowest value I was able to die at was 1 and that was with auto update being active. Now that I know how to work around it, it shouldn't be too much of an issue for me.

It was just highly frustrating trying to resolve thinking I was doing something wrong when in fact it just would not die on a health value less than 10 when the auto update was doing 10 damage a second and even when I reduced it to 1 damage a second and stopped the auto update at 0 it still would not kill me.

The only way I even resolved it was because I accident typed Value != MinValue and died right as I hit play then got curious as to what the lowest health value I could make m_Health.ImmediateDeath(); kill me at so I started using Value == # instead. Even when I did use the #, if the auto update was doing 10 DPS, then Value <=9 would not register and so I would not die. Value == 0 never registered and therefore Value <= MinValue would never work unless you had 1 as the min value and the Damage over time set to 1.
 
I mean, I changed it so that the auto update stopped when health was 0 and it still would not kill me at 0. The lowest value I was able to die at was 1 and that was with auto update being active. Now that I know how to work around it, it shouldn't be too much of an issue for me.

It was just highly frustrating trying to resolve thinking I was doing something wrong when in fact it just would not die on a health value less than 10 when the auto update was doing 10 damage a second and even when I reduced it to 1 damage a second and stopped the auto update at 0 it still would not kill me.

The only way I even resolved it was because I accident typed Value != MinValue and died right as I hit play then got curious as to what the lowest health value I could make m_Health.ImmediateDeath(); kill me at so I started using Value == # instead. Even when I did use the #, if the auto update was doing 10 DPS, then Value <=9 would not register and so I would not die. Value == 0 never registered and therefore Value <= MinValue would never work unless you had 1 as the min value and the Damage over time set to 1.
Hey there! I'm just wonder where you attach this script? I've been trying to get it to work in my own project with no avail.
 
Top