Skills based system

So, you’re saying the SkillExperience is not on the same game object as the Inventory?
Technically, there should only be one SkillExperience so you could, instead, do the same thing in there that we did in Inventory

// In SkillExperience.cs
public static SkillExperience GetPlayerSkillExperience()
{
    return FindObjectOfType<SkillExperience>();
}

and then use that to get the SkillExperience instead of GetComponent()

The SkillExperience is on the same game object as the inventory, but the resource gathering system isn’t

Yes, I know. So, playerInventory.GetComponent<SkillExperience>(); should give you the SkillExperience and will then not be null anymore. Yet it is. Why?

Aha! That was my mistake… I forgot to accumulate the ‘SkillExperience’ off the player, it was blindly trying to get it off the Resource Gatherer… thanks @bixarrio, apologies for my error :slight_smile:

My next step, if Brian doesn’t catch this chat yet, will be to integrate the difficulty function he mentioned when we were still developing ‘SkillFormula.cs’, the function that’s responsible for making these actions hard at the start, and they get easier as the levels increase. This function, in ‘SkillStore.cs’:

public bool SkillCheck(Skill skill, int levelRequired) {

            int skillLevel = GetSkillLevel(skill); 
            // difficulty to acquire something formula (tune as you desire) below:
            int bonus = (levelRequired - skillLevel) * 2;
            return UnityEngine.Random.Range(1, skillLevel) + bonus < levelRequired;

        }

It would be an amazing add-on for both combat and skills, to ensure the player doesn’t have it easy from the get-go (so your chances of landing a hit are low at the start, but they get better, and so is the case for resource gathering and other skills)

I’m trying to understand your HandleRaycast(). I don’t see how that could work. I see scenarios where it will work, but I also see scenarios where it doesn’t and then weird things start happening in my mind with regard to where everything will go wrong

HandleRaycast is only called on objects under the mouse, so

  • If I’m far from the tree and click it to chop it down, the player will start walking towards it. If I now move my mouse away, the player will walk up to the tree and do nothing. When I move the mouse over the tree again (not even clicking), the player will start chopping the tree.
  • If I’m far from the tree and click it to chop it down, the player will start walking towards it. If I now click somewhere else, the player will start walking somewhere else, abandoning the tree. However, as soon as I move my mouse over the tree again (not even clicking) the player will start moving towards the tree again to try and chop it down. If I did this to 90 trees, the player is going to be walking all over the place when I just move the mouse.

You also have an if saying;

  • if the player is more than 4m from the tree
    • start moving towards the tree
    • if the player is less than 4m from the tree
      • chop the tree

The second if will never execute because it is checked in the same frame as the code where you told it to move towards the tree - the player hasn’t moved yet - so it will never be closer than it was 1 line earlier when it was still more than 4m away

Looking at your ‘resource’, I would do something like this: Your ‘resource’ (tree) has a quantityPerHit that gives the player that amount when hitting the tree. Using the player’s current skill I would go one of two ways;

  • Make this the max quantity and adjust based on the player’s skill. If the skill is low, the player gets a lower quantity. You’d then probably want to store the max skill level required for max quantity. So, if you have quantityPerHit = 5; and maxSkillRequired = 10;, a player at level 5 will only get 2.5 (rounded) per hit, while a player at level 10 will get all 5. This would be amountToGive = Mathf.FloorToInt(quantityPerHit * Mathf.Clamp01(playerSkillLevel / (float)maxSkillRequired));
  • Determine a ‘drop chance’ based on the skill level. Again, you’d probably want to store a max skill level. So, if you have maxSkillRequired = 10; a player on level 1 will only have a 10% chance of getting the resource on a hit, while a player on level 5 will have a 50% chance and a player on level 10 will have a 100% chance. The check would be something like Random.value <= Mathf.Clamp01(playerSkillLevel / (float)maxSkillRequired);

Believe me when I say this, I have went through this walking-to algorithm a thousand times before I got a daytime job and tried fixing it to act properly multiple times, but I never got it to work properly. It was, honestly, a nightmare for me to even get this close, and then I tried tuning it multiple times to get it to work properly, but nothing worked well for me. Please, if you have any potential solutions that’ll make it work, share them with me :slight_smile: (I know we said I’ll try coding my own algorithms on my own, but I still do need help if I’m being fair)

You are spot-on with your predictions though… this Algorithm was a nightmare to develop.

Ooo I like this one. It’s spontaneous, has an element of surprise, and I definitely want this integrated into my systems… My question though is, where do we place/call this code? It was where I struggled with @Brian_Trotter 's skill function - Guess I’ll figure it out next :slight_smile:

Why do I have a feeling like this probability goes into a progression asset…?

In ResourceGathering. I’m going to change my ‘formula’ a little and allow for a minSkillRequired and a maxSkillRequired. If the player skill is below the minimum, they have no chance of a drop

public void Gather()
{

    Debug.Log("Gather Method Called");

    if (quantityLeft > 0)
    {
        // Play animation of whatever you're doing (mining, woodcutting, etc), and only stop when the resource source is dead
        // Find a way to kill the tree slowly, and improve the higher your level goes up (revise the defence algorithm in 'Health.cs')

        // Get the player skill level for this resource (need SkillStore for this)
        var playerSkillLevel = skillStore.GetSkillLevel(associatedSkill);
        // Determine the player drop chance based off their skill. We clamp it to between 0 and 1
        var playerDropChance = Mathf.Clamp01((playerSkillLevel - minSkillRequired) / (float)(maxSkillRequired - minSkillRequired));
        // Check if we will drop something here. If we do, add it to the inventory and reduce this resource
        if (Random.value <= playerDropChance)
        {
            playerInventory.AddToFirstEmptySlot(resourceToAccumulate, quantityPerHit);
            quantityLeft -= quantityPerHit;
        }
    }

    if (quantityLeft <= 0)
    {
        Destroy(gameObject);
        isDestroyed = true;
        Debug.Log(isDestroyed);
    }
}

Make sure minSkillRequired and maxSkillRequired does not have the same value, and that minSkillRequired is less than maxSkillRequired.

If I would take a wild guess, ‘minSkillRequired’ is the minimum level that the player needs before he can cut the tree, and ‘maxSkillRequired’ is the maximum level the player can achieve in the skill, right? (In my case, it’s probably level 200, don’t ask me why this number please…). I put these as ‘SerializeField’ values, so they can be tuned in the hierarchy… that’s what should be done, right? I’m sure these values aren’t being accumulated elsewhere

Idea on the fly though, why make the chances zero below a specific level when you can just lock the tree from being cut/rock being mined until the player reaches a specific level, similar to how we lock weapons/armor from being wielded until a specific level? Wouldn’t that be more direct?

Yeah, min would be the minimum skill level required - although at that level you still have a 0% chance of dropping because if you are on level 5 and the min is level 5 we’ll be checking 0 (5 - 5). You’d only really start getting a chance at min + 1. These are configured in the inspector per prefab. So, you could have a type of tree that’s easy to chop and another that’s harder and requires a higher level.

You could do that, but it’s extra code for the same thing.

Not sure what’s up with the formula, but something is off… even if the ‘maxSkillRequired’ is a low level, it’s still not working. I did the math on a side paper as well, it adds up in theory, and I re-checked the formula and it seems perfectly fine, but the compiler is just straight up not cutting down the tree :sweat_smile: (I apologize if I’m causing you any trouble). It’s like the formula is adamant on giving me a straight zero chance of getting a drop, even if my requirements are low/it’s an easy resource to hunt

Ahh nevermind… it works, just needed a little bit of further try and error testing :slight_smile:

True, but… it would be able to notify the player that something is happening, and if I were to integrate animations, it won’t just play them out of boredom, right?

For now though, I’ll call this a day and try figure out the handle raycast, because frankly speaking, I really want to fix this algorithm… I agree with you that it’s whacky, but I tried my best to get it to act right, but it failed :confused: (thanks again @bixarrio)

If this last line of this comment isn’t deleted, it means I’m still struggling to fix the ‘walk-to’ algorithm (the algorithm responsible for having the player run to the source before resource gathering)

Or a modified DropLibrary…

Doesn’t make sense tbh… aren’t DropLibraries for drops? I’m all ears for new ideas :slight_smile: - I had plans to use them to modify the type of drops individual enemies drop, never thought of them as progression assets… Regardless, his new formula does quite well, but my Raycast method can really use some guidance so my forests don’t turn into a stack overflow :stuck_out_tongue_winking_eye: (been trying for a while now…)

RandomDropper is for drops. DropLibrary provides a level based probability of getting certain drops. Rather than using the player’s level, you’d use the player’s relevant skill level.

Think of the Gatherer like the Fighter. The HandleRaycast just tells the Gatherer “I want to gather this item”. The Gatherer then accepts that as a target, and if it’s too far away it gathers, otherwise it moves towards the resource. This Gatherer would be an IAction, so it can be cancelled by clicking away.

Once there, and ready to gather, face the resource and call whatever animation trigger you need to get the relevant gather animations going. Personally, at that point I would use a Coroutine to gather the resource. At the end of the Coroutine, give the player rewards out of the DropLibrary. If the action is cancelled before the end of the Coroutine, just clear the target and call StopAllCoroutines().

Exactly what I was thinking. The fighter doesn’t start hitting as soon as you click. It moves to the target first. Same with gathering, so you’d do the same thing you did with fighter, but with a different end goal; gathering instead of fighting

2 Likes

OK so thanks to you guys reminding me that there’s an interface, Brian’s detailed steps on how to make this work (I tried following it as closely as I possibly can in the code below. Once I get the gather to automatically work after the player arrives, I’ll start thinking of the coroutines), and my intriguing nature taking over to re-write this function, I came up with something significantly cleaner. This is my ‘HandleRaycast()’ function now:

// When you click on the resource source (tree/rock/etc...), run this code:
        public bool HandleRaycast (PlayerController callingController) {

            if (Input.GetMouseButtonDown(0))
            {
                if (!GetIsInRange(playerInventory.transform)) {
                    playerInventory.GetComponent<Mover>().StartMoverAction(this.transform.position, 1.0f);
                    if (GetIsInRange(playerInventory.transform)) Gather();
                }
                else {
                    playerInventory.GetComponent<ActionSchedular>().CancelCurrentAction();
                    Gather();
                }

            }
            return true;
        }

        public bool GetIsInRange(Transform playerTransform) {

            return Vector3.Distance(transform.position, playerTransform.position) <= acceptanceRadius;

        }

I took some inspiration from fighter and mover, but not the whole way through (and I stole @bixarrio 's idea to get sneaky with the reference to the player components, by referring to the sole character in this game that has an inventory on him… the player (I learned this recently… xD))

However, similar to the previous lunatic function I had before, this one still does not gather resources once the player reaches the tree. I tried tuning the interiors of the function to get it to work multiple times (one of them had me use a ‘while’ loop, and that crashed my compiler, the other one had me trying to integrate ‘HandleRaycast()’ in ‘Gather()’, not the opposite way around, and that made things worse, boolean flags didn’t work well, and don’t ask me about what happened when I tried getting the ‘Update()’ method involved… the game became an automatic resource gathering machine, with the right timers set up), but I still can’t get my head around how to get that. Any idea what to fix in this function to make it work? :slight_smile:

I was thinking more of a Gatherer component that would exist on the Player
The HandleRaycast would simply tell the Gatherer to gather this item. HandleRaycast should be doing as little as possible in this process, much like CombatTarget.

The Gatherer’s Update would manage the moving and begin the gathering process.

What Brian said.

I said this earlier; the HandleRaycast is only called for objects under the mouse. It’s not a continuous update. You can’t be doing distance checks and decisions on when it is time to gather. You need to have a Gatherer component like the Fighter component. Your player is a fighter, and now it is also a gatherer.

Look at the Fighter component. It does all the distance checks, triggers the animation, applies the damage, etc. It’s also an IAttack which gets used by the ActionScheduler, so it will cancel other actions when it is activated, and get cancelled itself when another action is started. You want all your gathering logic in there; the moving to the resource, the animations, the awarding of XP and resources, etc.

Your resource (ResourceGathering.cs) will now be very similar to the CombatTarget component. It does nothing more than supply which cursor to show and tell Gatherer to start the gathering action

OK so… I tried implementing a ‘ResourceGatherer.cs’, which uses information assigned in ‘ResourceGathering.cs’, and then attempts to manipulate the player to interact with it, but I’m not sure how this is going so far. Can any of you have a look and kindly advise me on what changes I can do to make the function work? Please :slight_smile:

Here is my ResourceGathere.cs script:

using GameDevTV.Inventories;
using RPG.Core;
using RPG.Movement;
using RPG.ResourceManager;
using RPG.Skills;
using UnityEngine;

public class ResourceGatherer : MonoBehaviour, IAction
{

    [SerializeField] int acceptanceRadius;  // minimum distance that the player has to enclose, before he can begin gathering resources
    [SerializeField] ResourceGathering resourceToHunt;  // the resource the player is aiming for
    [SerializeField] Inventory playerInventory; // a convenient method to access the players' inventory

    private SkillExperience skillExperience;
    private SkillStore skillStore;
    public bool isResourceDestroyed = false;

    public void Awake() {

        playerInventory = GetComponent<Inventory>();
        skillExperience = playerInventory.GetComponent<SkillExperience>();
        skillStore = playerInventory.GetComponent<SkillStore>();

    }

    public bool CloseToSource() {
        return Vector3.Distance(transform.position, resourceToHunt.transform.position) <= acceptanceRadius;
    }

    public Skill GetAssociatedSkill() {
        return resourceToHunt.GetAssociatedSkill();
    }

    public int GetExperienceValue() {
        return resourceToHunt.GetExperienceValue();
    }

    public void Gather() {

        if (!CloseToSource()) GetComponent<Mover>().MoveTo(resourceToHunt.transform.position, 1.0f);
        
        if (resourceToHunt.GetQuantityLeft() > 0) {
            
            // Difficulty Setup:
            var playerSkillLevel = skillStore.GetSkillLevel(GetAssociatedSkill());
            var playerDropChance = Mathf.Clamp01((playerSkillLevel - resourceToHunt.minSkillRequired)/(float)(resourceToHunt.maxSkillRequired - resourceToHunt.minSkillRequired));

            // if (UnityEngine.Random.value <= playerDropChance) {
            playerInventory.AddToFirstEmptySlot(resourceToHunt.ResourceToAccumulate(), 1);
            resourceToHunt.quantityLeft -= 1;
            AccumulateXP();
            // }

        }

        else
        {
            Destroy(resourceToHunt);
            isResourceDestroyed = true;
            Debug.Log(resourceToHunt.isDestroyed);
        }

    }

    public void AccumulateXP() {
        skillExperience.GainExperience(GetAssociatedSkill(), GetExperienceValue());
    }

    public void Cancel()
    {
        GetComponent<Mover>().Cancel();
    }
}

And this is my ‘ResourceGathering.cs’ script (I’m yet to delete stuff from that, but first I want to ensure my gatherer works properly):

using UnityEngine;
using GameDevTV.Inventories;
using RPG.Control;
using RPG.Movement;
using RPG.Skills;
using RPG.Core;
using UnityEngine.AI;
using Unity.VisualScripting;

// Steps to get the experience:
// 1. Declare a 'SkillExperience' variable
// 2. get the component of the variable in 'Awake()'
// 3. Split the type of XP you get, based on the tag... for trees, it's "Tree", and for Rocks, it's "ResourceRocks"
// 4. When gathering, call 'skillExperience.GainExperience()', so you get the experience you are supposed to get when gathering resources
// 5. Play and test multiple times...

namespace RPG.ResourceManager
{
    public class ResourceGathering : MonoBehaviour, IRaycastable, IAction
    {
        [SerializeField] InventoryItem resourceToAccumulate;    // the loot we are getting from the tree, rock, or whatever we're harvesting
        [SerializeField] int quantityPerHit = 1;    // how many of that loot is being acquired, per mouse click
        private float acceptanceRadius; // how close do we need to be to the resource source (tree, rock, etc...) before we can interact with it
        public int quantityLeft = 10;   // how many resources are in that source, before it's destroyed and re-instantiated

        Inventory playerInventory;  // the target inventory for our resources to go to (the player's inventory in this case)
        
        public bool isDestroyed = false;    // flag, to kill the source if it's out of resources
        bool isMovingTowardsResourceObject = false;   // flag, to ensure that we are close to the source of the resources, before being able to gather resources from it

        // skill experience link, to get the skill, and fine-tune it according to the trained skill
        SkillExperience skillExperience;
        [SerializeField] Skill associatedSkill;
        [SerializeField] int experienceValue;
        [SerializeField] CursorType cursorType;

        [SerializeField] ResourceGatherer resourceGatherer;

        SkillStore skillStore;

        private NavMeshAgent navMeshAgent;

        void Awake()
        {            
            playerInventory = Inventory.GetPlayerInventory();
            if (playerInventory == null) Debug.Log("Player Inventory Not Found");

            // In very simple terms, when you got the playerInventory, you got a hold of the only in-game
            // component that actually has an 'Inventory.cs' script on him, the player. 
            // As the player has a hold of the 'SkillExperience' script as well, you can catch it
            // through his inventory as well, since you got a hold of him through his inventory, 
            // so that's what I did below:
            skillExperience = playerInventory.GetComponent<SkillExperience>();

            // Just like how we accumulated 'skillExperience' above, we accumulate
            // 'skillStore' as well with the exact same method (getting the object (Player)
            // that the only inventory in the game is attached to):
            skillStore = playerInventory.GetComponent<SkillStore>();

            // Same thing to get the NavMeshAgent...
            navMeshAgent = playerInventory.GetComponent<NavMeshAgent>();
            
            acceptanceRadius = 4.0f;
            isMovingTowardsResourceObject = false;

            resourceGatherer = playerInventory.GetComponent<ResourceGatherer>();

        }

        [SerializeField] public int minSkillRequired;
        [SerializeField] public int maxSkillRequired;

        public int GetQuantityLeft() {
            return quantityLeft;
        }

        public InventoryItem ResourceToAccumulate() {
            return resourceToAccumulate;
        }

        public void Gather()
        {

            Debug.Log("Gather Method Called");

            if (quantityLeft > 0)
            {
                // Play animation of whatever you're doing (mining, woodcutting, etc), and only stop when the resource source is dead
                // Find a way to kill the tree slowly, and improve the higher your level goes up (revise the defence algorithm in 'Health.cs')

                // Get the player skill level for this resource (you'll need 'SkillStore.cs' for this):
                var playerSkillLevel = skillStore.GetSkillLevel(associatedSkill);
                // Determine the player drop chance, based off their skill level... It will be clamped between 0-1
                var playerDropChance = Mathf.Clamp01((playerSkillLevel - minSkillRequired)/(float)(maxSkillRequired - minSkillRequired));
                // Check if we have the chance of getting a resource. If we do, add it to the inventory, get your XP, and reduce the quantity left off the source:
                // if (Random.value <= playerDropChance) {
                // Player gathering resource algorithm:
                playerInventory.AddToFirstEmptySlot(resourceToAccumulate, quantityPerHit);
                quantityLeft--;
                AccumulateXP();
                // }
            }

            if (quantityLeft <= 0)
            {
                Destroy(gameObject);
                isDestroyed = true;
                Debug.Log(isDestroyed);
            }
        }

        public Skill GetAssociatedSkill() {
            return associatedSkill;
        }

        public int GetExperienceValue() {
            return experienceValue;
        }

        public void AccumulateXP()
        {            
            Debug.Log("Accumulate XP Called");
            skillExperience.GainExperience(associatedSkill, experienceValue);
        }

        public CursorType GetCursorType()
        {
            if (isDestroyed == true) return CursorType.None;
            return cursorType;
        }

        /* public bool HandleRaycast(PlayerController callingController)
        {

            // Resource Gathering Logic:

            // A. If your mouse is clicked:
            // 1. if you are far away from the tree, get closer and then cut it down ('Gather()')
            // 2. if you were already close to the tree, just cut it down already... ('Gather()')
            
            // B. If your mouse is not clicked, but when you clicked it earlier you were already moving towards the tree, and you are now close to it:
            // 1. You're close, so just cut it down already... ('Gather()')

            if (Input.GetMouseButtonDown(0)) {

                float distanceToTree = Vector3.Distance(transform.position, callingController.transform.position);

                if (distanceToTree > acceptanceRadius)
                {
                    Debug.Log("Moving towards the tree");
                    callingController.GetComponent<Mover>().MoveTo(transform.position, 1.0f);
                    isMovingTowardsResourceObject = true;
                    
                    if (isMovingTowardsResourceObject && distanceToTree <= acceptanceRadius) {

                        Debug.Log("Cutting this stupid ass tree down...");
                        Gather();
                        isMovingTowardsResourceObject = false;
                        
                        }

                    return true;

                }

                else if (distanceToTree <= acceptanceRadius)
                {
                    Debug.Log("Cutting down the tree...");
                    Gather();
                    isMovingTowardsResourceObject = false;
                    return true;
                }

            }

            if (isMovingTowardsResourceObject && Vector3.Distance(transform.position, callingController.transform.position) <= acceptanceRadius)
            {
                Gather();
                isMovingTowardsResourceObject = false;
                return true;
            }

            return true;

        } */

        public void Cancel()
        {
            // Implementation of 'IAction.cs'
            
            // (Similar to both skillExperience and 'skillStore', we accumulate the mover from the only script that has
            // an 'inventory' component on it, the player. We can get the player through his tag, but I'm too lazy, and a bit dodgy:
            // If the action is cancelled, stop the navMeshAgent:
            playerInventory.GetComponent<Mover>().Cancel();
        }

            /* // When you click on the resource source (tree/rock/etc...), run this code:
            public bool HandleRaycast(PlayerController callingController)
            {

            // Early Note: if you read what 'Start()' and 'Update()' had to say about the 'skillExperience', 'skillStore'
            // and 'navMeshAgent', we got these components off the only character that has an inventory on him, the player,
            // under the name of 'playerInventory()'. We use that exact same reference here as well

                    if (Input.GetMouseButtonDown(0))
                    {

                    if (!GetIsInRange(playerInventory.transform))
                    {
                        // If the player is not close enough to this tree, make him move towards it:
                        playerInventory.GetComponent<Mover>().MoveTo(this.transform.position, 1.0f);
                        isMovingTowardsResourceObject = true;
                    }

                    else
                    {
                        // If you're in range, gather the resources:
                        playerInventory.GetComponent<ActionSchedular>().CancelCurrentAction();
                        Gather();
                        isMovingTowardsResourceObject = false;
                    }
                }
                return true;
            }

            public bool GetIsInRange(Transform playerTransform)
            {
                return Vector3.Distance(transform.position, playerTransform.position) <= acceptanceRadius;
            } */

            public bool HandleRaycast(PlayerController callingController) {

                /* if (Input.GetMouseButtonDown(0)) {
                    Gather();
                } */
                if (Input.GetMouseButtonDown(0)) {
                resourceGatherer.Gather();
                return true;
                }
                return true;
            }
        }
    }

Privacy & Terms