Skills based system

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;
            }
        }
    }

This is nothing like the Fighter component. Your player is going to start gathering from the resource long before it gets there. It may even destroy the resource before it gets there. You also implemented IAction but for no reason. You’re not doing anything that requires this component to have the IAction interface

Here’s what ResourceGatherer needs:

  1. A few serializeble fields
    • Acceptance Radius is good
    • Maybe some value defining the delay between each ‘gather’
  2. A few private fields
    • The ResourceGathering component as the target
  3. A Gather(ResourceGathering target) method that is called from the HandleRaycast
    • This method sets the target, and calls StartAction on the ActionScheduler, passing this as the action
  4. An Update method that
    • does nothing if the target is null
    • checks if the target is in range (use the acceptance radius)
      • if it’s not, tell the mover script to move to the target
      • if it is, cancel the mover and start gathering resources - probably in a Coroutine
  5. Implement IAction
    • in the Cancel
      • Set the target = null
      • Cancel the mover

I haven’t given the actual coroutine much thought, but there will need to be some interaction in the coroutine between ResourceGatherer and ResourceGathering. Perhaps the gatherer will ask the gathering how many resources it gets now, and do this on every interval until the action gets cancelled (IAction.Cancel will have to also StopAllCoroutines) or the resources run out.


Edit On the fly and untested, but it should be this simple. Also much more like Fighter and without a coroutine

using RPG.Core;
using RPG.Movement;
using UnityEngine;

public class ResourceGatherer : MonoBehaviour, IAction
{
    [SerializeField] int acceptanceRadius;
    [SerializeField] float timeBetweenGathers;
    
    private ResourceGathering target;
    private float timeSinceLastGather = Mathf.Infinity;
    
    private void Update()
    {
        timeSinceLastGather += Time.deltaTime;
        
        if (target == null) return;
        
        if (!GetIsInRange(target.transform))
        {
            GetComponent<Mover>().MoveTo(target.transform.position, 1f);
        }
        else
        {
            GetComponent<Mover>().Cancel();
            GatherBehaviour();
        }
    }
    
    public void Gather(ResourceGathering target)
    {
        GetComponent<ActionScheduler>().StartAction(this);
        this.target = target;
    }
    
    public void Cancel()
    {
        target = null;
        GetComponent<Mover>().Cancel();
    }
    
    private bool GetIsInRange(Transform targetTransform)
    {
        return Vector3.Distance(transform.position, targetTransform.position) < acceptanceRadius;
    }
    
    private void GatherBehaviour()
    {
        transform.LookAt(target.transform);
        if (timeSinceLastGather < timeBetweenGathers) return;
        // TODO: Do a single gather here
        timeSinceLastGather = 0f;
    }
}

A gather could be something like

  1. Get the required skill from the gathering
  2. Get the player level for that skill
  3. Ask gathering how many resources I can get with that skill level
    • If any, add it to the inventory
    • gathering may also remove it from its own available count

Here’s how I would do this. Much of this is boilerplate.
This is a basic framework, it doesn’t handle difficulty beyond checking level to see if your level is high enough, and for now it only returns the one item. Lots of room for improvement.

using GameDevTV.Inventories;
using RPG.Control;
using RPG.Skills;
using UnityEngine;

namespace RPG.Gathering
{
    public class GatherableNode : MonoBehaviour, IRaycastable
    {
        [SerializeField] private InventoryItem itemToGather;
        [SerializeField] private Skill requiredSkill;
        [SerializeField] private int requiredLevel;
        [SerializeField] private float gatheringRange = 2;
        [SerializeField] private float gatheringTime;
        
        public float GatheringRange => gatheringRange;
        public float GatheringTime => gatheringTime;
        public Skill RequiredSkill => requiredSkill;
        public int RequiredLevel => requiredLevel;

        public CursorType GetCursorType()
        {
            return CursorType.Gathering;
        }

        public bool HandleRaycast(PlayerController callingController)
        {
            if (callingController.TryGetComponent(out Gatherer gatherer) && gatherer.CanGather(this))
            {
                if (Input.GetMouseButton(0))
                {
                    gatherer.StartGatheringAction(this);
                }
                return true;
            }
            return false;
        }

        public InventoryItem GetItem()
        {
            return itemToGather;
        }
    }
}
using System;
using System.Collections;
using GameDevTV.Inventories;
using RPG.Core;
using RPG.Movement;
using RPG.Skills;
using UnityEngine;

namespace RPG.Gathering
{
    public class Gatherer : MonoBehaviour, IAction
    {
        private GatherableNode target;
        private SkillStore skillStore;
        private ActionScheduler actionScheduler;
        private Inventory inventory;
        private Mover mover;
        private Animator animator;

        private bool isGathering;
        
        private void Awake()
        {
            actionScheduler = GetComponent<ActionScheduler>();
            inventory = GetComponent<Inventory>();
            mover = GetComponent<Mover>();
            animator = GetComponent<Animator>();
            skillStore = GetComponent<SkillStore>();
        }

        private void Update()
        {
            if (!target) return;
            if (Vector3.Distance(transform.position, target.transform.position) > target.GatheringRange)
            {
                mover.MoveTo(target.transform.position, 1.0f);
            }
            else
            {
                mover.MoveTo(transform.position,0);
                Gather();
            }
        }

        private void Gather()
        {
            if (isGathering) return;
        }

        private IEnumerator GatherRoutine()
        {
            isGathering = true;
            animator.SetTrigger("Gather");
            yield return new WaitForSeconds(target.GatheringTime);
            inventory.AddToFirstEmptySlot(target.GetItem(), 1);
            isGathering = false;
        }

        public bool CanGather(GatherableNode node)
        {
            if (!mover.CanMoveTo(node.transform.position)) return false;
            if(skillStore.GetSkillLevel(node.RequiredSkill)<node.RequiredLevel) return false;
            return true;
        }
        
        public void Cancel()
        {
            target = null;
            StopAllCoroutines();
            isGathering = false;
        }

        public void StartGatheringAction(GatherableNode gatherableNode)
        {
            actionScheduler.StartAction(this);
            target = gatherableNode;
        }
    }
}
1 Like

GTA 6 Trailer is coming next month, just saying :stuck_out_tongue_winking_eye:

Anyway… Here is what I managed to come up with, integrating @bixarrio 's suggestion to my code, in ‘ResourceGatherer.cs’:

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;
    [SerializeField] float timeBetweenGathers;

    private ResourceGathering target;
    private float timeSinceLastGather = Mathf.Infinity;

    private SkillStore skillStore;
    private Inventory playerInventory;
    private SkillExperience skillExperience;

    private void Awake() {
        skillStore = GetComponent<SkillStore>();
        playerInventory = GetComponent<Inventory>();
        skillExperience = GetComponent<SkillExperience>();
    }

    private void Update() {

        timeSinceLastGather += Time.deltaTime;

        if (target == null) return;

        if (!GetIsInRange(target.transform)) GetComponent<Mover>().MoveTo(target.transform.position, 1.0f);
        else {
            GetComponent<Mover>().Cancel();
            GatherBehaviour();
        }

    }

    public void Gather(ResourceGathering target) {
        GetComponent<ActionSchedular>().StartAction(this);
        this.target = target;
    }

    private bool GetIsInRange(Transform targetTransform) {
        return Vector3.Distance(transform.position, targetTransform.position) < acceptanceRadius;
    }

    private void GatherBehaviour() {

        transform.LookAt(target.transform);
        if (timeSinceLastGather < timeBetweenGathers) return;
        // Gather Function here
        timeSinceLastGather = 0f;

    }

    public void Gather() {

        if (target.quantityLeft > 0) {

            var playerSkillLevel = skillStore.GetSkillLevel(target.GetAssociatedSkill());
            var playerDropChance = Mathf.Clamp01((playerSkillLevel - target.minSkillRequired)/(float)(target.maxSkillRequired - target.minSkillRequired));

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

        }

    }

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

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

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

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

I’ll try @Brian_Trotter 's suggestion next :slight_smile: (if it works, it works… as long as I don’t gather resources from a million miles away xD). Frankly speaking though, I loved @bixarrio 's idea of the woodcutting difficulty reduction over the levels (I still believe it needs a ‘level to unlock’ though, because what’s the point of trying to cut a tree if you won’t be able to cut it anyway… at least a notification system perhaps?). It makes a lot of sense when used right (I’ll see how I can mix both up and how we can improve this system down the line)

@bixarrio whenever you have the time, can you please double-check my ‘ResourceGathering.HandleRaycast()’ method (I don’t mean this in any offensive way, hopefully it doesn’t come through negatively, just asking for a favor because I can’t figure it out)? Just want to make sure we got this one right at least :slight_smile:

Hi Brian, first of all as usual thanks for investing the time to help us develop a gathering functional algorithm… However, it did not work :sweat_smile: (as in, when I hover over the tree, I get the “None” cursor (for this one I was planning to use bixarrios’ suggestion to return the cursorType that the enum chooses), and when I click on it, the player doesn’t even attempt to go there at all, let alone attempt to gather resources). There are no NREs or any sort of errors, it just… doesn’t work

Just to be clear, the ‘Gatherer’ goes on the player, and the ‘GatheringNode.cs’ goes on the resource tree/rock, right? Should I continue developing this (now I’m baffled)? :slight_smile:

Edit: @bixarrio 's formula worked after a little bit of further try and error, it just needs a little more fine-tuning and code cleaning xD (the both of you have components the other is missing though. So for example whilst @bixarrio has what works best for me, Brian has a ‘Level to Unlock’. Bixarrio has a difficulty factor, which I absolutely love and want in my game, but Brian has animation integrated and Coroutines, etc)… I want to cross-match both of your algorithms :laughing: (I’ll re-confirm this after a little further testing, but so far so good. I will spend some time cleaning up my code now, and understand what his Algorithm did differently from my original attempt)

Something fun I would personally touch on this topic with, is to check that we have a pickaxe in our hands, because… well… we’re ‘Woodcutting’, not ‘treePunching’ (knock knock @bixarrio :stuck_out_tongue_winking_eye: ). That’ll be my personal mini-challenge :slight_smile:

Something fun I tried doing though, is increasing the chance of getting multiple logs at once, and the chance of more logs increases as your skill level increases. I tried coding this function individually, and this is what I came up with:

public int RandomizeAmountPerLevel(int level) {

        int amount = 0;

        if (level >= 1 && level <= 15) {
            amount = Random.Range(1,10);
            target.quantityLeft -= amount;
        }

        else if (level >= 16 && level <= 30) {
            amount = Random.Range(1,2);
        }

        else if (level >= 31 && level <= 45) {
            amount = Random.Range(1,3);
        }

        else if (level >= 46 && level <= 60) {
            amount = Random.Range(1,4);
        }

        else if (level >= 61 && level <= 75) {
            amount = Random.Range(1,5);
        }

        else if (level >= 76 && level <= 90) {
            amount = Random.Range(1,6);
        }

// You get the idea...

        return amount;

    }

Any suggestions (ideas) to make it better? Once we get the animations out of the way (now I’ll probably be forced to buy an animation pack soon, here we go… Once I get an animation pack I’ll integrate it into this system, call each animation according to the skill, and make sure everything works well :slight_smile: )

You added a public Gather() that is now being called by your HandleRaycast(), so it removes anything I did in the class. Make this Gather() private, and call it from within GatherBehaviour() where the // Gather Function here comment is. Then look at Brian’s GatherableNode. His HandleRaycast calls gatherer.StartGatheringAction(this). This is what you should do, except with my example call gatherer.Gather(this).

Brian gave you a far more complete example. I just gave you the basics of what the gatherer/gathering may be

The first option in this post does this; the higher your skill, the more resources it drops

The second Gather() function was differentiated from the first one, as the second one had no parameters. To fix the issue, I used this in ‘ResourceGathering.cs’ in the Handle Raycast (I drastically cleaned it up, now that I know it’s all what happens under the cursor):

public bool HandleRaycast(PlayerController callingController) {
                if (Input.GetMouseButtonDown(0)) {
                resourceGatherer.Gather(this);
                }
                return true;
            }

I loved the both of your work, now I just want to mix them up to come up with something nice. So far, we have already gone through the most annoying part in my eyes… I’ll probably buy new animations tomorrow and start properly integrating them into my game (won’t go nuts with the money yet, gotta ensure the code works properly first). For now though, I managed to add in a ‘level check’ system that ensures that you can’t cut trees until you reach a specific level, and it relies on ‘minSkillRequired’:

private int levelToUnlock;

// Some code here...

public int GetLevelToUnlock() {
            return levelToUnlock = minSkillRequired;
        }

        public bool HandleRaycast(PlayerController callingController) {
                if (Input.GetMouseButtonDown(0)) {
                if (skillStore.GetSkillLevel(associatedSkill) < GetLevelToUnlock()) return false;
                resourceGatherer.Gather(this);
                }
                return true;
            }

Only issue is, you need to subtract 1 from your desired level to unlock to get it to work in the inspector (alls good though, this one doesn’t bug me much, because unlocking it earlier ensures your probability of getting any drop is zero… so this formula is ideal imo), so a tree locked to level 60 for instance won’t allow you to touch it until level 59, and only at 60 will you get a slim chance of making it work

Will check this again and update you, thanks @bixarrio :slight_smile:

Edit: I thought we’d place that line, this one:

// test line below:
                int amountToGive = Mathf.FloorToInt(target.GetQuantityPerHit() * Mathf.Clamp01(playerSkillLevel/(float)target.GetMaxSkillRequired()));
                target.quantityLeft -= amountToGive;

in ‘ResourceGatherer.Gather()’, right before deducting from our tree the amount of resources left, but apparently that didn’t work well in ‘Gather()’, and then deduct ‘amountToGive’ from the targets’ ‘quantityLeft’ variable. If we’re doing this though, I was thinking of a random, probabilistic model, so that the player has his/her chances of getting more than one item when his level is higher, making the game a little more exciting as well… (I’m guessing nested conditions now?). So let’s go back to the ‘level 60 tree’ example, what I’m trying to do here is at levels 60-65 for example, your chances of getting 2 logs are slightly less than someone at level 66, but someone for example at level 87 is far more likely to get 2 logs, but less likely to get 3 logs than… idk… someone at level 115 for example (I tried figuring it out on my own before school, but I’m out of time right now)

I didn’t mention that you’d need to add the CursorType.Gathering to your CursorTypes, and you would need to assign that CursorType in your PlayerController to a cursor or the PlayerController will set a CursorType it can’t find in the Array to CursorType.None

You could add a field to the GatheringNode with the correct CursorType for that resource (just be sure to add it to CursorTypes and to add that to the Cursors in the PlayerController.

Your RandomizeAmountPerLevel gives you potentially more items (up to 10) when you’re low level than it does in the highest level.

I’ll show you a practical example of using a DropLibrary to not only increase the potential amount of the resource the character gets, but to return useless items as well. A great example is in World Of Warcraft. When you try to fish in an area that you are still weak for, you are far more likely to catch an old boot than you are a fish, and the higher the level, the better the fish you catch.

Alright, let’s start then :blush: - thanks in advance for taking the time to help me out again (although bixarrio’s algorithm is robust, apart from the missing need for animation triggers, but I’m guessing that’s an easy copy paste from “Fighter.cs”. So if we can use bixarrio’s algorithm as a starting point, that’d be great, but if there’s a complete override then by all means that’s fine as well). We only get experience for catching fish, and not pointless stuff though, right?

For this one, I wanted a cursor for every type of activity to be individual of the rest, and bixarrio showed me how to do that earlier. Sure, we know that the activities are the same, but the player doesn’t need to know that :stuck_out_tongue_winking_eye:

that’s the point I was hoping for :slight_smile:

It was just a rough attempt (and I admit I was probably wrong… :laughing:) I’ll give @bixarrio 's suggestion a run before school now (Edit: I’m questioning where his line of code goes for the moment… and I’m out of time and haven’t figured that out yet…)

It feels like Brian and I are now off on two different paths here. Brian said he’ll show you how to do it with a drop table, and you keep asking me to expand on my suggestion - which has nothing to do with the drop tables.

That is where it will go.

You now want a combination of the two options I suggested earlier. First, you want to see if the player gets any logs at all and if they do, you want to have a chance of dropping more than 1. So, you keep the first part and we’ll need to figure out a way to possible increase the number of logs.

I’m spitballing now, but a naïve way would be to approach the player level a bit like a ‘weight’. Let’s assume a player needs to have a minimum level of 10 to chop the tree - giving them 1 log, and the tree will give a max of 5 logs per hit, depending on the player’s level. Let’s also say that the minimum level a player needs to have any chance of getting 5 logs is 20. So, a player with level 10 may get 1 log and a player with level 20 will have a much higher chance of getting 5 logs - but not guaranteed.

Let’s see… Uhm… Trying to organise my thoughts…

We’ll now pick a random number between 0 and the player’s skill level. We’ll also split the maximum number of logs between each level. This may give us fractions, but we will sort that out again later. We will continue to add the fractions to an accumulator until the random number is ‘used up’. Players with higher skill levels will ‘use’ it for longer, giving it more resource fractions

private int DetermineAmountToGive(int playerSkillLevel, int minLevel, int maxLevel, int maxResourcesPerHit)
{
    var randomValue = Random.value * (playerSkillLevel - minLevel)
    var fractionPerLevel = maxResourcesPerHit / (maxLevel - minLevel);
    var amountToGive = 1f; // a float because we use fractions
    for(var i = 0; i < (maxLevel - minLevel); i++)
    {
        randomValue -= i; // 'use' random value
        amountToGive += fractionPerLevel;
        if (randomValue <= 0f) // random value is 'used up'
        {
            break;
        }
    }
    return Mathf.FloorToInt(amountToGive);
}

amountToGive is initialised to 1 to ensure at least 1 log and because the result will not exceed maxResourcesPerHit - fractionPerLevel

I guess I’ve been totally overthinking this… And my math isn’t great… I think players with nearly max level may also have a chance of getting the max resources

Privacy & Terms