Resource Gathering for Third person

OK so… you might be aware that I once had a Resource Gathering system I developed for my RPG, based on the point-and-click system, but now that we’re slowly transitioning to third person, my system needs to transition ahead as well…

I never actually had an idea of how mining, woodcutting, etc… works for third person games

Can I please get some insights on how these works, or what players prefer in modern Survival/RPG games? I plan to use State Machines for my approach to this problem, I just need ideas on what to consider when developing this system

I don’t mind sharing my current scripts as well, if necessary

There’s little difference really. The important thing is knowing that you have a node nearby and then calling mining/gathering on that node.

You already know you probably want an animation while the character gathers the resource…

So you’ll need a Finder for the Resource, that will look a LOT like the Finder for pickups…
Once you determine you should gather the resource (be it a dedicated key or another if statement added to the PlayerFreeLookState when trying the attack key), you’ll want to call the PlayerFacingState, passing in a reference to a new gathering state which will call an animation and respond to that animation to gather the resource. It’s the exact same workflow as the Pickup state.

Will give that a go and keep you updated on how it goes, but is mapping the keys really that necessary? I feel like that one generic handleAttack button function we had is more than enough for our game needs, and I was hoping on the long term that we can eliminate all the pointless key maps and just interact with our surroundings using one button, and have UI show up to tell us what we’re able of interacting with, based on what’s closest to us in the environment instead

BTW I hope you also saw my latest issues regarding the enemy states :slight_smile:

I approached this a very different way when i was working in 2d. i have not refactored my gathering to 3d yet but i have thought about it and id really just add a raycast to the system below. i didnt use a state machine i used observer. I’m traveling right now so i don’t have the code handy. but this is how it works…

A small note before hand here. i have an iGatherable interface that inherits from idamagable. the nodes all have very high health but take very high damage in the gather check as “decay”. if you hit them with normal weapons the y take damage (which might cost you an attempt) but nowhere near the damage they take as gather decay.

I used a collider attached to the gather node.

when the player is in the collider it gives a visual queue that a resource node is close. in my case it was an exclamation, for a 3d maybe just a particle effect twinkle you have stored in an object pool, or an audio queue with the player centered on the resource node.

when the player looks at the resource node( you would just use a raycast here but i didn’t need it for a 2d …i just used a smaller collider) they get a prompt to interact.

interact fires an event passing through the object it is interacting with . a gather checker script then checks if the players level in that type of gathering if so it performs a gather. if not it it fires an event that is picked up with an animation and text telling the player they are too low level to harvest this item.

if the check is passed. the gather checker then does a roll which you could tweak however you want, but i defaulted mine to a 75% chance of success with a 10% chance of crit and a 10% chance of crit failure.
if it crit fails the node is destroyed
if it fails the node takes damage and the player gets no resources.
if it succeeds the node takes damage and the player gets resources
if it crits the the node takes NO damage and the player gets resources.

when the tree health gets to 0 the node is out of resources and dies.

i used events so i could easily hook up audio queue, call an animation, and reuse my damage function as a node decay. but if you aren’t comfortable with an observer pattern it will work fine just putting the checker script on the player and passing through the nodes info directly to the mono stored on the player.

1 Like

I believe I mentioned “or another if statement added to the PlayerFreeLookState when trying the attack key”
As you add new actions that can be done with the one button, add them to the If statements.

In terms of UI, In the RangeFinder class (which is the parent of all the Finder classes) is a pair of events:

    public event System.Action<T> OnTargetAdded;
    public event System.Action<T> OnTargetRemoved;

Your UI can subscribe to these events to activate icons to represent what is available.
For example, for Pickups, you can subscribe to PickupFinder.OnTargetAdded/OnTargetRemoved events (You’ll need a method that takes in a Pickup in this case), and then turn the UI on or off if PickupFinder.HasTargets==true.

Hint: Each of the Finders OnTargetAdded event will take in whatever type T is, Pickup for PickupFinder, AIConversant for ConversantFinder, etc.

heya, sorry for the late response. Literally, today was a dramatic day :stuck_out_tongue_winking_eye: - but hey, I had fun

I didn’t understand much, to be frank, but I loved the idea of giving the tree health, and collecting logs based on its health. For the success rate, I remember me and @bixarrio spent a few hours thinking of an Algorithm that defaults chances of success from 0 to 1, based on whether you are a low or high level

I think the only thing I’ll probably need to do, not sure yet, is apart from setting up the animations in a new controller, would be to give the tree health (now I see why @Brian_Trotter and bixarrio named it “TreePunching”, “RockBashing”, etc… :laughing:), and then re-wire it all up in the Controls and State Machines

Might need to write a new State Machine script though, and even an IJsonSaveable capture and restore token states, because if respawn times are long, they need to be saved (might also accidentally drag bixarrio back into this (sorry man :laughing:), because… well… he’s good with global times :stuck_out_tongue_winking_eye: )

fair enough, but my main question is… if I don’t want all these other buttons involved (I want to use them as resources for other actions (perhaps), and to keep the game simple), what would I have to delete? (I really need to revise that section of the code…)

putting that request aside for a second, the resource gathering system might be the biggest change tbh. Here’s what I want to do (after a little bit of thinking):

  1. Give the tree, or rock, health. Just like an enemy, it’s punchable/hurtable, until it dies, with random damage (based on your woodcutting level for a tree, or mining level for a rock. The higher these values, the stronger and the normal combat targeting), and you may or may not get a log/resource out of it (depends on your specific skill level, for rocks = mining, for trees = woodcutting)

  2. When you’re near one, and you press the targeting state button (which is the caps lock for me, since tab is taken by construction, but this caps lock is now taken by both resource focus and combat focus… depending on what you find within radius (If you find both, there will be a future trigger for determining if an enemy is aggressive or not. If he is aggressive, focus on him first. If not, and you’re not in combat, focus on that first, otherwise if you’re being pursued, focus on the enemy first)

I mainly want it this way so the player doesn’t miss hitting the resource sources (trees/rocks)), you’ll get into the combat targeting state, but this time it’ll be for a resource source (tree/rock), so I’m guessing you’ll need a target.cs script on that tree, but how do we get this to load (because when you currently get close to one and hit the targeting button, it’s not working… I probably forgot something. Besides, I don’t want ‘Target.cs’ to rely on ‘Health.cs’ for a tree, mainly because I’d like to create a new ‘ResourceHealth.cs’ script for that special case)

  1. You can’t strike trees, rocks or other resource elements unless you have specific equipment in-hand (and the right skill level). For trees, maybe a special type of axe. For rocks, a special type of pickaxe. For fishing, a fishing rod, etc

  2. A global timer, and a saveable entity. If some of these resources will take hours to respawn, it might be a good idea to respawn the based on the global timer of our computer, instead of a local one for the Unity engine (I think I know how to do this, just putting it out here…)

I’ll probably give this whole thing a go in the morning, but until then, how do we approach this, and do we need any code? I plan to eliminate my ‘ResourceGathering.cs’ script and ‘ResourceGatherer.cs’ script, in replacement for new ones, but I need suggestions on how to do that

So far I only implemented a ‘ResourceFinder.cs’, which connects to my ‘ResourceRespawner.cs’ scripts’ “ITarget()” interface

I’m not sure if my approach is the best for Resource Gathering off trees or rocks, but I’m just guessing for now (I’m open for new ideas as well)

Quite literally, just delete the actions in the Action Map.
And don’t subscribe to the related events in the PlayerFreeLookState. It’s really that simple.

The individual keys are there as an option, and can help disambiguate between items when there is a Pickup, and an AIConversant within range of the finders. You don’t have to use them.

1 Like

Wouldn’t it make more sense for it to look for the Resource itself?

Yup, so here’s an update. I attached the ‘ResourceHealth.cs’ script I created (check below) to the resource itself, as per your suggestion, and connected the ‘ResourceFinder.cs’ script to it, so now we can only find trees/resources that are still alive

My problem now is… how do I even start coding that? Having old scripts and planning new ones can be a bit of a head-dazzle for me (I want to connect this script to the State Machines, so it’s one go)

(so far I implemented a new ‘ResourceHealth.cs’ script, which is responsible for the health of the tree). This is what it looks like (so far):

using UnityEngine;

public class ResourceHealth : MonoBehaviour
{
    [SerializeField] int maxHealth;
    private int currentHealth;

    private void Start() 
    {
        currentHealth = maxHealth;
    }

    public void TakeDamage(int damage) 
    {
        currentHealth -= damage;
        if (currentHealth <= 0) Die();
    }

    private void Die() 
    {
        // Add in Particle system, sounds, etc... Death event
        Destroy(gameObject);
    }
}

I’d say we start with the targeting state next, since it sounds the most complex. It’ll use the same targeting camera as the one we use for targeting our enemies, but it doesn’t work with the resource for some reason, even with ‘Target.cs’ attached to the resource

And unlike the weapons, I want a function like “TryHit()” (which we are using for our weapons, but now for resource gathering) to strike anything it touches (that can be done by triggering the function event on the animation, based on what we are currently hunting for), as long as the weapon is in action

I know this is a complex one, so I’m trying it step by step, and more than happy to be patient about it :slight_smile: (I tried writing a ‘ResourceTargeter.cs’ and ‘ResourceTarget.cs’ scripts as well, but they both didn’t work as expected… not going to open 5 problems out at once again, step by step we go)

Have you not found it tedious to keep writing the same code over and over? The ResourceHealth looks exactly like any other Health script. Why have another one? If you want specific things to happen when specific objects die, you can either

  • Inherit from the Health and override Die()
public class Health : MonoBehaviour
{
    [SerializeField] protected int maxHealth;
    protected int currentHealth;

    protected virtual void Start()
    {
        currentHealth = maxHealth;
    }

    public void UpdateHealth(int amount)
    {
        currentHealth = Mathf.Clamp(currentHealth + amount, 0, maxHealth);
        if (currentHealth == 0) Die();
    }

    protected virtual void Die()
    {
        // Do generic Die stuff
        Destroy(gameObject);
    }
}

public class ResourceHealth : Health
{
    protected override void Die()
    {
        // Do the resource specific 'Die' things
        base.Die();
    }
}

or

  • Add UnityEvents and invoke them when the target dies
public class Health : MonoBehaviour
{
    [SerializeField] int maxHealth;
    [SerializeField] UnityEvent onDie;
    private int currentHealth;

    private void Start()
    {
        currentHealth = maxHealth;
    }

    public void UpdateHealth(int amount)
    {
        currentHealth = Mathf.Clamp(currentHealth + amount, 0, maxHealth);
        if (currentHealth == 0) Die();
    }

    private void Die()
    {
        onDie.Invoke();
        Destroy(gameObject);
    }
}

Note that I use UpdateHealth(...) because it allows me to both heal (UpdateHealth(10)) and damage (UpdateHealth(-10)) the target

I thought it can be simpler as well, but I have a major problem with coding… when a concept is too big, it can overwhelm me a bit :sweat_smile: (but this isn’t one I would’ve figured out on my own tbh)

(P.S: is ‘Die()’ that you mentioned above the function we eventually renamed to ‘UpdateState()’ at the end of the course series? I’m a little confused on where which script goes, as my ‘Health.cs’ looks a bit… way too different :sweat_smile:)

If it helps, here is my ‘Health.cs’ script (although be warned, it’s a messy one… there was a lot of testing functionalities in there that I’m keeping, in case I ever need them):

using UnityEngine;
using GameDevTV.Saving; // for being able to save the health component of our scenes, when necessary
using RPG.Stats;    // to access the health of our Player/Enemy, in the Start() function
using RPG.Core;     // to access 'ActionSchedular' from 'RPG.Core' namespace
using GameDevTV.Utils;  // allows us to use LazyValue (to help stuff work right after 'Awake()' function, before 'Start()')
using UnityEngine.Events;
using Newtonsoft.Json.Linq;
using RPG.Skills;
using RPG.Combat;
using RPG.Control;
using System.Collections.Generic;
using System.Linq;
using System;

namespace RPG.Attributes {

    public class Health : MonoBehaviour, IJsonSaveable {

        [SerializeField] float regenerationPercentage = 70; // when we level up, we want to regenerate our health to 70% of our new levels' maximum health
        public TakeDamageEvent takeDamage; // allows us to display Damage UI when we hit/get hit by something the player can fight
        public UnityEvent onDie;
        // for the 'CheckForSkill()' function:
        private WeaponConfig weaponConfig;
        

        public event System.Action<GameObject> onTakenHit;

    [System.Serializable]
    public class TakeDamageEvent : UnityEvent<float> {

        // This class exists to enable us to have a 'TakeDamage' Event in the Unity Inspector under this script,
        // allowing us to call a bunch of events when our player takes damage (affecting the damage audio and displaying damage text)

    }
    
    // LazyValue is what you use when you want to only initialize a value, only when its ready for first use (not during Start() or Awake() for example)
    // This is useful for variables that might be performance-heavy for example:
    LazyValue <float> healthPoints;   // Starting health of Player
    
    bool wasDeadLastFrame = false;

    private void Awake() {
         healthPoints = new LazyValue<float>(GetInitialHealth);
    }

    // ----------------------------- OUT OF COURSE CONTENT: Since this game is now based on a Skilling system, the code here gets the average of which Skill did the most damage, so that 'AwardExperience() (verion 4)' can use it ---------------------

    /*

    // If you want to only award XP to the highest-damaging skill during combat, use this with version 4 of 'AwardExperience()':



    // 1. start off with an empty dictionary. This dictionary will hold the value of the damage caused by each skill, and the Key being the Skill causing that damage:
    private Dictionary<Skill, float> damagePerSkill = new Dictionary<Skill, float>();

    // 2. Create an 'AddDamage()' function. This function will store the damage implemented by the skills, and add them up, so that they can be collected by 'GetMostDamagingSkill()':
    private void AddDamage(Skill skill, float damage) {

        // If we don't have a key for our skill in the 'damagePerSkill' dictionary, assign one (to be equal to the damage):
        if (!damagePerSkill.ContainsKey(skill)) damagePerSkill[skill] = damage;
        // if we have a key, increment the skill with the damage
        else damagePerSkill[skill] += damage;
    }

    // Accumulate the most damaging skill, by going through the dictionary and getting the top value (so we can award Experience to that skill):
    private Skill GetMostDamagingSkill() {
        
        // using what we did in 'AddDamage' (it's all combined in 'Health.TakeDamage()' below), we order the skills in our 'damagePerSkill' in Descending order
        // and we return the skill at the top of the list (as that's the skill that did the most damage)
        return damagePerSkill.OrderByDescending(s => s.Value).First().Key;

    }

        */

    // ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

    // -------------------------------------------- MORE OUT OF COURSE CONTENT: If you want to split your XP to ensure everyone that does damage gets XP, then use this ----------------------------------------------------------------------

    // If you want to equally split your skills' XP to all the combat skills that did damage in a fight, use this instead, with 'AwardExperience()' version 5:

    private Dictionary<Skill, float> damagePerSkill = new Dictionary<Skill, float>();

    private List<Skill> damageOrder = new List<Skill>();

    private void AddDamage(Skill skill, float damage) {

            // 1. Order the skills from the first used to the last:
            if (damageOrder.IndexOf(skill) < 0) damageOrder.Add(skill);
            // if the skill doesn't exist, add it to the dictionary, and assign a value to it:
            if (!damagePerSkill.ContainsKey(skill)) damagePerSkill[skill] = damage;
            // otherwise add damage to the currently existing skill damage:
            else damagePerSkill[skill] += damage;

    }

    private Skill GetMostDamagingSkill() {

        // Place the most damaging skill to the top of your dictionary, and convert that dictionary to a list:
        var damagingSkill = damagePerSkill.OrderByDescending(s => s.Value).ToList();
        // If only one skill did the damage, just award that skill (.Key) all the RewardXP
        if (damagingSkill.Count == 1) return damagingSkill[0].Key;
        // If two combat skills did similar damage, then rank them accordingly, through this loop, in steps:
        if (Math.Abs(damagingSkill[0].Value - damagingSkill[1].Value) < 0.01f) {
            for (int i = 0; i < damageOrder.Count; i++) {

                if (damageOrder[i] == damagingSkill[0].Key) return damageOrder[i];
                if (damageOrder[i] == damagingSkill[1].Key) return damageOrder[i];

            }

        }

        // For the compiler to run:
        return damagingSkill[0].Key;

    }

    // ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

    private float GetInitialHealth() {

        // anything the LazyValue aims for must take NO ARGUMENTS, and RETURN FLOAT!
        return GetComponent<BaseStats>().GetStat(Stat.Health);

    }
    
    public void Start() {

        healthPoints.ForceInit();
        // This line forcefully accesses health right now, if it hasn't been accessed in Awake() or LazyValue yet

    }

    private void OnEnable() {

        GetComponent<BaseStats>().onLevelUp += RegenerateHealth;    // subscribing RegenerateHealth() to 'onLevelUp' event Action created in 'BaseStats.cs'
                                                                    // (so that we can regenerate part of our health when we level up)
        }
    
    private void OnDisable() {

        // Disabling avoids Memory Leaks
        GetComponent<BaseStats>().onLevelUp -= RegenerateHealth;    // subscribing RegenerateHealth() to 'onLevelUp' event Action created in 'BaseStats.cs'
                                                                    // (so that we can regenerate part of our health when we level up)

        }

    public bool IsDead() {

        // This function simply verifies if our target is dead or not. If he is, we can stop attacking him
        // If not, continue fighting to death (this is a 'public getter' method)

        return healthPoints.value <= 0;

    }

    // OUT OF COURSE PARAMETER: Skill skill, as the player now inputs 'skills' to gain XP according to the damage he did
    // (the beauty here is that 'TakeDamage' now gives the player XP based on the weapon he used to win the war):
    public void TakeDamage(GameObject instigator, float damage, Skill skill) {

    // Check what type of weapon the player is holding, so the game knows what type of skill to train him up for:
    // skill = weaponConfig.CheckForSkill();

    print(gameObject.name + " took damage: " + damage);

        // This function clamps the health of the player, based on taken damage
        // and if the target is dead, we want them to die and give our player some XP
        // (using the 'AwardExperience(GameObject instigator)' function)

        healthPoints.value = Mathf.Max(healthPoints.value - damage, 0); // Clamps our health value to never go negative, neither above what our player currently has

        takeDamage.Invoke(damage);        // invoking a 'TakeDamage' event (if our player is not dead),
                                          // so when our player gets hit by an enemy, we want to display
                                          // the text of the damage taken

        onTakenHit?.Invoke(instigator);     // if you get hit, you automatically fight back!

        // Adding damage count to the skill currently doing the damage, so we can, by the end of the fight, award
        // the fight experience to the skill that did the most damage:
        AddDamage(skill, damage);

        // when the player dies:
        if (IsDead()) {

        onDie.Invoke(); // Invokes external UnityEvent Audio 'Die', combined with other events, when our Enemy/Player has taken enough damage to die
        // AwardExperience(instigator); // (SECOND 'AwardExperience()' FUNCTION) Award experience to whoever won the battle
        // AwardExperience(instigator, skill);    // Award experience to whoever won the battle, for the chosen skill (for the player obviously) that won the battle with (Attack, Defence, Mage or Range)

        // Based off the damage accumulated by 'AddDamage(skill, damage)' above, we now award experience
        // after the fight to the skill that has done the most damage within the fight:
        AwardExperience(instigator, GetMostDamagingSkill());
        
        GetComponent<ActionSchedular>().CancelCurrentAction();
        // onDie.Invoke(); // Invokes external UnityEvent Audio 'Die', combined with other events, when our Enemy/Player has taken enough damage to die

            // This UpdateState() is an addon here, to ensure the player truly dies on his death:
            UpdateState();

            }

            // ADDON LINE RIGHT BELOW THIS COMMENT:
            // else takeDamage.Invoke(damage);

            // ORIGINAL: remove else, and delete the 'UpdateState()' in 'if(IsDead()) {}'
            else UpdateState();  // Update the Players' health state

    }

    public void Heal(float healthToRestore) {

        // This function ensures the player never exceeds maximum health when he eats (to heal)
        healthPoints.value = Mathf.Min(healthPoints.value + healthToRestore, GetMaxHealthPoints()); // keeps us below the maximum health points
            
            // Update the Players' health state
            UpdateState();

    }

    public float GetHealthPoints() {
        return healthPoints.value;
    }

    public float GetMaxHealthPoints() {
        return GetComponent<BaseStats>().GetStat(Stat.Health);
    }

    public float GetPercentage() {
        // This function returns how much percentage of health is available for the player
        // before he's dead

        return 100 * (healthPoints.value / GetComponent<BaseStats>().GetStat(Stat.Health));
    }

    public float GetFraction() {
        // To display our Enemies' health on a healthbar, we want to divide the percentage of his health by 100 (again):
        return (healthPoints.value / GetComponent<BaseStats>().GetStat(Stat.Health));
        }

    private void UpdateState() {

        // This function is the Death function of the Player/Enemy, just renamed:
        
        Animator animator = GetComponent<Animator>();

        // if we weren't dead last frame, but now we died, then trigger the animator, cancel actions, update your death, and Invoke 'onDie()'
        if (!wasDeadLastFrame && IsDead()) {

        animator.SetTrigger("die");

        // Cancelling all executable scripts if we die
        GetComponent<ActionSchedular>().CancelCurrentAction();

        // Invoke death ONLY IF THE PLAYER WASN'T DEAD BEFORE, BUT NOW HE'S BEEN RECENTLY KILLED (nah, don't do that. It glitches the Item Drops...):
        // onDie?.Invoke();

            }

        if (wasDeadLastFrame && !IsDead()) {

            // if he was dead, but now has been respawned, reset the animator using 'Rebind()' function
            animator.Rebind();  // Reset the Animator entirely, and start from scratch

        }

        // Updating the Death of whoever carries this script (Player/Enemy):
        wasDeadLastFrame = IsDead();
    
    }

        /* private void AwardExperience(GameObject instigator) {

            // This function rewards experience to our combat instigator, by first
            // getting the experience component, checking if its null or not, and if its not
            // we want to add the experience to our current player experience
            // (if the experience is null, quit this function)

            Experience experience = instigator.GetComponent<Experience>();

            if (experience == null) return;

            experience.GainExperience(GetComponent<BaseStats>().GetStat(Stat.ExperienceReward));

            } */

        /* private void AwardExperience(GameObject instigator)
        {
            // The replacement function of my 'AwardExperience' takes care of the RARE event of
            // having a Hit() function being fired when the player is supposed to be dead.
            // In other words, if you're dead, you can't attack the guard, you must die immediately!

            if (instigator.TryGetComponent(out Experience experience)) {

                if (instigator.TryGetComponent(out Health otherHealth) && !otherHealth.IsDead()) {

                    experience.GainExperience(GetComponent<BaseStats>().GetStat(Stat.ExperienceReward));

                }

            }

        } */

        // NOTE: OPTIONAL PARAMETERS ALWAYS COME AFTER THE REQUIRED PARAMETERS, AND ARE ALWAYS ASSIGNED IN THE FUNCTION PARAMETERS!!!

        /// <summary>
        ///  This third iteration of 'AwardExperience does two major changes...
        ///  1. It replaces the Combat 'Experience' based system of our player to a brand new 'Skilling' system,
        ///  so now the player can individually train attack, defence, ranged or magic, independent of each other,
        ///  giving them flexibility into the type of character they want their player to develop into...
        ///  2. Instead of relying on the Experience Reward in a "Progression" Asset, the system now relies on the 'XPReward' variable
        ///  in 'AIController.cs', so each enemy can have their individual XP Reward on death, rather than relying on a progression bar
        ///  (both options are amazing either way)
        /// </summary>
        /// <param name="instigator"></param>
        /// <param name="skill"></param>
        /* private void AwardExperience(GameObject instigator, Skill skill) {

            if (instigator.TryGetComponent(out SkillExperience skillExperience)) {

                if (instigator.TryGetComponent(out Health otherHealth) && !otherHealth.IsDead()) {

                    skillExperience.GainExperience(skill, Mathf.RoundToInt(GetComponent<AIController>().GetXPReward()));

                    // If you don't want to assign individual XP to your enemies, use the below formula instead (and change them up in 'Progression' attached to each individual enemy):
                    // skillExperience.GainExperience(skill, Mathf.RoundToInt(GetComponent<BaseStats>().GetStat(Stat.ExperienceReward)));

                }

            }

        } */

        /// <summary>
        /// This fourth iteration of 'AwardExperience' attempts to split the total XP Reward over the primary combat skill to train, and
        /// Defence. Generally, Defence gets 1/3rd of the total XP Reward, and whatever skill (based on the weapon held by the player) gets
        /// the other 2/3rd of the total XP Reward. By default, if 'other skill' is not fed anything, then it is the 'Defence' skill
        /// </summary>
        /// <param name="instigator"> </param>
        /// <param name="skill"> </param>
        /// <param name="otherSkill" </param>
        /* private void AwardExperience(GameObject instigator, Skill skill, Skill otherSkill = Skill.Defence) {

            if (instigator.TryGetComponent(out SkillExperience skillExperience)) {

                if (instigator.TryGetComponent(out Health otherHealth) && !otherHealth.IsDead()) {

                    // This line sends 2/3rd of the XP Reward to whichever skill is associated to the Players' weapon:
                    skillExperience.GainExperience(skill, 2*Mathf.RoundToInt(GetComponent<AIController>().GetXPReward()/3));
                    // This line sends 1/3rd of the XP Reward to the defence XP, which is always 1/3rd of whatever the AI Reward XP is assigned to:
                    skillExperience.GainExperience(otherSkill, Mathf.RoundToInt(GetComponent<AIController>().GetXPReward()/3));

                }

            }

        } */

        /// <summary>
        /// The fifth and final draft of 'AwardExperience()' attempts to split the XP done by each combat skill during the fight fairly amongst
        /// the skills that took part in the fight (and defence still gets, by default, 1/3rd of the XP)
        /// </summary>
        /// <param name="instigator"> </param>
        /// <param name="skill"> </param>
        /// <param name="otherSkill"> </param>
        private void AwardExperience(GameObject instigator, Skill skill, Skill otherSkill = Skill.Defence)
        {

            if (instigator.TryGetComponent(out SkillExperience skillExperience))
            {

                if (instigator.TryGetComponent(out Health otherHealth) && !otherHealth.IsDead())
                {

                    float totalReward = GetComponent<AIController>().GetXPReward();
                    float totalDamage = damagePerSkill.Values.Sum();

                    foreach (var pair in damagePerSkill) {
                        float relativeValue = pair.Value / totalDamage;
                        //Sending 2/3rd of the entire XP Reward to the other skills, and splitting them up according to the damage they did during the fight:
                        skillExperience.GainExperience(pair.Key, 2*Mathf.RoundToInt(totalReward*relativeValue)/3);
                    }

                    // This line sends 1/3rd of the XP Reward to the defence XP, which is always 1/3rd of whatever the AI Reward XP is assigned to:
                    skillExperience.GainExperience(otherSkill, Mathf.RoundToInt(totalReward / 3));

                }

            }

        }

        private void RegenerateHealth()
        {

            // This function is being subscribed to 'onLevelUp' event created in 'BaseStats.cs',
            // so we can regenerate part (or all) of our health when we level up our player

            // Setting our new health (after levelling up) to be equal to healthPoints * regenerationPercentage (regenerationPercentage = 70%)
            float regenHealthPoints = GetComponent<BaseStats>().GetStat(Stat.Health) * regenerationPercentage / 100;

            healthPoints.value = Mathf.Max(healthPoints.value, regenHealthPoints);
        }


    /* public object CaptureState() {

        // Saving the Health (healthPoints) of the player
        return healthPoints.value;


    }

    public void RestoreState(object state) {

        // Steps:
        // 1. Reclaim the CaptureStates' healthPoints value, and cast it from a Dictionary value to a float type
        // 2. Update the Enemies' health state (i.e: if he's dead, he's dead... (for non-respawnable enemies, that is...!))

        // Loading the health of the player:
        // this is done by casting/converting his health into a float first, so that float healthPoints
        // can store the value

        healthPoints.value = (float)state;

            // Die();          // This function ensures if a guard was dead on the last save, to keep him dead 
                               // (DUE TO THE INVOKE METHOD INVOLVED IN Die(), AND THAT INITIATES A RANDOM DROPPER, WHICH LEADS TO A GAME
                               // BUG, WE HAD TO MANUALLY IMPLEMENT THE ACTION CANCELLATION AND TRIGGER SETUP HERE INSTEAD!)
                               // Having that stupid invoke was responsible for infinity loot drop from RandomDropper(), so we eliminated it...

            UpdateState();
            /* GetComponent<Animator>().SetTrigger("die");
            GetComponent<ActionSchedular>().CancelCurrentAction();

        } */

    public JToken CaptureAsJToken() {
        return JToken.FromObject(healthPoints.value);
    }

    public void RestoreFromJToken(JToken state) {
        healthPoints.value = state.ToObject<float>();
        UpdateState();
        }

    }

}

I’m just not sure about how to get the death of a tree in ‘Health.cs’ tbh…

You used Die() so I used Die()

You have packed that Health full of player specific stuff and have now restricted yourself from reusing the code. Enemies and trees and rocks don’t need to get xp or regen health when they level up. All you need is the health I posted above, and from there you can extend it to cover the player, enemies, resources, structures, whatever you want to do damage to.

Here is a simple example

Example
public abstract class Health : MonoBehaviour
{
    protected int currentHealth;

    protected virtual void Start()
    {
        currentHealth = GetMaxHealth();
    }

    public void UpdateHealth(int amount)
    {
        currentHealth = Mathf.Clamp(currentHealth + amount, 0, GetMaxHealth());
        if (currentHealth == 0) Die();
    }

    protected abstract float GetMaxHealth();
    protected virtual void Die()
    {
        // Do generic Die stuff
        Destroy(gameObject);
    }
}

public abstract class EntityHealth : Health
{
    protected override float GetMaxHealth()
    {
        // Just an example of getting the max health from some stats system
        return StatsSystem.Instance.GetStat(Stat.BaseHealth);
    }
}

public class PlayerHealth : EntityHealth
{
    protected override void Die()
    {
        // Example for game over
        GameManager.Instance.GameOver();
        // do the base stuff
        base.Die();
    }
}

public class EnemyHealth : EntityHealth
{
    private float GetExperienceAward()
    {
        // Example of getting experience for the enemy
        return StatsSystem.Instance.GetStat(Stat.Experience);
    }

    protected override void Die()
    {
        // Example for awarding XP to the player
        Player.AwardXP(GetExperienceAward());
        // do the base stuff
        base.Die();
    }
}

public class ResourceHealth : Health
{
    [SerializeField] float maxHealth;
    protected override float GetMaxHealth()
    {
        return maxHealth;
    }
}

It’s just an example, but it shows that you don’t have to copy all the code everywhere. All the basic health code is in Health, you don’t need to write it over and over. Some more specialised code go in EntityHealth for things that get their health from a stats system. That’s all it does different; get the max from the stat system. Then there’s the specialised version for EnemyHealth that awards experience when it dies (I’d probably have an event instead, but this is an example) and a ResourceHealth that don’t get its max from a stat system, but is specified in the inspector. That’s all the code that’s different.


Unsolicited Review

Also:

  • Start() should probably not be public
  • You may want to start looking at cohesion. You have 4 (maybe more) methods that are all related to each other
public float GetHealthPoints()
{
    return healthPoints.value;
}
public float GetMaxHealthPoints()
{
    return GetComponent<BaseStats>().GetStat(Stat.Health);
}
public float GetPercentage()
{
    // This function returns how much percentage of health is available for the player
    // before he's dead

    return 100 * (healthPoints.value / GetComponent<BaseStats>().GetStat(Stat.Health));
}
public float GetFraction()
{
    // To display our Enemies' health on a healthbar, we want to divide the percentage of his health by 100 (again):
    return (healthPoints.value / GetComponent<BaseStats>().GetStat(Stat.Health));
}

It doesn’t have to be all over the place. If anything needs to change, you now have to find all the places you do the same thing and change it there, too. If it’s more cohesive, you only need to change the code in one place

public float GetHealthPoints()
{
    return healthPoints.value;
}
public float GetMaxHealthPoints()
{
    return GetComponent<BaseStats>().GetStat(Stat.Health);
}
public float GetPercentage()
{
    // This function returns how much percentage of health is available for the player
    // before he's dead
    return GetFraction() * 100f;
}
public float GetFraction()
{
    // To display our Enemies' health on a healthbar, we want to divide the percentage of his health by 100 (again):
    return GetHealthPoints() / GetMaxHealthPoints();
}

Alright then… I’ll try re-factoring the function and keep you updated, I just hope this doesn’t mess things up more than it solves things (not a trust issue, just that I don’t know how the code will work over there, after the modifications…)

You don’t have to change it. I was just making an observation and letting you know you are making things harder for yourself. It’s very likely going to break things

Honestly… I didn’t even think of that before… I’m just not extremely familiar with code refactoring just yet, so I figured I’d just re-write that script for ‘ResourceHealth.cs’, because for now, it sounded easier… (I know it’s not the ideal situation, but re-factoring that script can be a bit challenging for me, and this one is more likely to truly break things than fix them… I’m not sure, but for the moment, unless you or Brian are willing to observe what I do, I’d rather stick with my current code to avoid breaking things)

For now, I scrapped the old scripts and started from scratch. Here’s the ‘ResourceFinder.cs’:

using System.Collections;
using System.Collections.Generic;
using RPG.Core;
using UnityEngine;

namespace RPG.ResourceManager {

public class ResourceFinder : RangeFinder<ResourceHealth> {}

}

and ‘ResourceHealth.cs’:

using System.Collections;
using System.Collections.Generic;
using RPG.Attributes;
using RPG.Core;
using UnityEngine;

public class ResourceHealth : MonoBehaviour, ITarget
{

    private float health;

    bool ITarget.IsValid() 
    {
        return health > 0f;
    }
}

Just waiting for Brian (because I tried targeting, health and scripting other stuff today, and nothing worked for me…)

Eventually, these are my goals:

  1. Give the tree, or rock, health. Just like an enemy, it’s punchable/hurtable, until it dies, with random damage (based on your woodcutting level for a tree, or mining level for a rock. The higher these values, the stronger and the normal combat targeting), and you may or may not get a log/resource out of it (depends on your specific skill level, for rocks = mining, for trees = woodcutting). I was thinking of adding a ‘Weapon Damage’ factor, so that weapons used for long eventually need repairing, but that’s a bit far fetched right now…

  2. When you’re near one, and you press the targeting state button (which is the caps lock for me, since tab is taken by construction, but this caps lock is now taken by both resource focus and combat focus… depending on what you find within radius (If you find both, there will be a future trigger for determining if an enemy is aggressive or not. If he is aggressive, focus on him first. If not, and you’re not in combat, focus on that (the resource) first, otherwise if you’re being pursued, focus on the enemy first), switch to the targeting camera and focus on the resource source closest to the center of the camera (similar to what the code for ‘Targeter.cs’ did)

I mainly want it this way so the player doesn’t miss hitting the resource sources (trees/rocks)), you’ll get into the combat targeting state, but this time it’ll be for a resource source (tree/rock), so I’m guessing you’ll need a target.cs script on that tree, but how do we get this to load (because when you currently get close to one and hit the targeting button, it’s not working… I probably forgot something. Besides, I don’t want ‘Target.cs’ to rely on ‘Health.cs’ for a tree, mainly because I’d like to create a new ‘ResourceHealth.cs’ script for that special case… which means I’ll probably have to re-write the entire Health.cs for the resource for that special case)

  1. You can’t strike trees, rocks or other resource elements unless you have specific equipment in-hand (and the right skill level). For trees, maybe a special type of axe, at a specific level and above. For rocks, a special type of pickaxe. For fishing, a fishing rod, etc… (you get the idea, levelling up)

  2. A global timer, and a saveable entity. If some of these resources will take hours to respawn, it might be a good idea to respawn the based on the global timer of our computer, instead of a local one for the Unity engine (I think I know how to do this, just putting it out here…)

You’re asking for way too much out of the gate. I’d sort of assumed you had the resource gathering done and needed a way to adapt it to the third person.

All of these things need to be taken one step at a time. Complex systems usually don’t pop in out of the block. You have have noticed, for example, in Nathan’s course we start with a State and slowly build upon the concent until eventually we get a PlayerFreeLookState, and eventually other states as well.

So we’re going to take this back a step and clearly define what exactly a Gatherable is, and build up step by step gathering, then we’ll add health, then we’ll add respawning and saving, then we’ll add specific weapon needs, then we’ll add skills…

In all of these cases, we’re not going to move on to the next step until the previous step is working properly.

And here’s the fun part, I’m now the lead engineer, and you are the junior engineer on this Gathering project. This doesn’t mean I write the whole thing (because as the lead engineer, I’ve got irons in a lot of other fires, my job is to keep you on track with your job and assign you tasks to complete). Each task will consist of:

  • A clearly defined goal //Set all consideration of future goals aside and focus on the goal at hand
  • A brief explanation of what is expected in this goal
  • A challenge to you, the Junior engineer to write code to achieve this goal
  • Some form of testing to ensure that our goal is achieved.
  • You refining the code until that specific test passes.

Note that you’re going to encounter things along the way that don’t appear to “fit the plan” right away. That’s ok. They will eventually get there.

This is also something that will take some time. Once a goal is put forth, I’m putting another goal up for at least a day, and in no event will the next goal be put up before the current goal is completed.

And here is a thing that I think is important because of your use of a Unity specific concept, the concept of a Resource. As you know, Unity has special folders named Resources and an entire class named Resources that is used to ensure certain objects are packaged with the game and are accessible through the Resources.Load methods. Naming a gatherable item a “Resource” or anything close to that will eventually lead to confusion. This very concept actually derailed Sam’s initial prototype for Core Combat as he started putting the Health scripts in a folder named Assets/Scripts/Resources and put the Health in the RPG.Resources namespace. This led to problems and refactoring along the way. Let’s nip that in the bud right now (since we’re starting from scratch here). Let’s refer to anything that can be gettable through gathering (mining, tree chopping, etc) as a Gatherable.. Our new namespace will be RPG.Gatherables.

Challenge 1: The GatherableItem

For the Gatherable Item, you'll need a ScriptableObject inherited from InventoryItem (because InventoryItems can't be instantiated) that represents the Gatherable.
  • It will need an asset menu entry [CreateAssetMenu(filename="new Gatherable Item", menuName = "RPG/Inventory/Gatherable Item")]
  • It will need a Serialized field that links to a GameObject prefab
  • It will need a spawn method that takes in a Transform, spawns the prefab as child to the transform and returns that value as GameObject.

So that’s your challenge, to write this ScriptableObject.

The testing for this stage will be to use the Asset Menu to create a new Gatherable Item in a Resources folder (like we would do for any Inventory Item) and link a Prefab with a model that you want to be in the scene to the Serialized GameObject field in the Gatherable.

Paste in your code for this, and a screenshot of the GatherableItem’s inspector with all of it’s fields filled out.

1 Like

I believe I said that these were long term goals (no offence meant here btw, I apologize if this came along the wrong way). I wanted to start simple with just the health and targeter (idk if that was complex or not), and then expand on that… I don’t mean to sound defensive, just explaining my perspective :slight_smile:

THIS IS EXACTLY WHAT I’VE BEEN SEEKING FOR A WHILE NOW. I got a ton of experience from reading the external scripts we wrote, I just want someone to brief me on the ideas of how things are done, and observe how I do it, so if I get lost somewhere down the line, I can be re-guided. That’s how I want to learn, and I’d be incredibly happy if we stick together till the end

Anyway, I’ll give the first challenge a go, and keep you updated with my results :slight_smile: (and I’m okay with waiting a day between each challenge, it’s extremely fair :slight_smile:)

At this point in time, I won’t judge. I’ll follow along, and look back at it all in the end

Rome wasn’t built in a day and night, and neither will any amazing game be… xD

I don’t know what’s going on there, but I pretty much avoid naming any unnecessary folders as ‘Resources’ since the time where not having an InventoryItem in a resources folder messed with some of my stackable items. I recall that event pretty well :laughing:

Nothing specific is happening yet by naming things ResourceHealth, etc, but it can easily lead to issues down the road. I just find it best to avoid anything with Resource in the name in our projects, aside from the Resources folder itself where we put things we need the Resources.Load to get at runtime.

Privacy & Terms