Resource Gathering for Third person

Fair enough. Anyway, I think I got the challenge done, and I’ll brief a few things if necessary :slight_smile:

  1. Here’s the ‘GatherableItem.cs’ script:
using GameDevTV.Inventories;
using UnityEngine;

[CreateAssetMenu(fileName = "new Gatherable Item", menuName = "RPG/Inventory/Gatherable Item")]

public class GatherableItem : InventoryItem
{
    [SerializeField] GameObject gatherableItem;


    private GameObject SpawnMethod(Vector3 spawnPoint) 
    {
        var spawnedItem = Instantiate(gatherableItem, spawnPoint, Quaternion.identity);
        return spawnedItem;
    }
}
  1. Here’s the screenshot from the Unity Engine:

  1. The ‘Gatherable item’ assigned in the screenshot above refers to a Pickup Item, something I created when I was testing the pickup system for wood, when creating the resource gathering system for the old point-and-click game system… I’m not sure if that’s what was expected or not, as the GameObject data type is a generic one, but I was following along with this instruction (also please check if the resources path is correct or not):

GameObject prefab

Maybe thtis can help you on get down the right path.
in my game the the resource nodes, players, and enemies all use the same health script. that script has an interface called idamagable with a TakeDamage method on it

my igatherable interface is just an extension of idamagable with a method called for TakeGatherDecay.

a player might attack for 10 damage. an enemy might have 50 health, but the tree has 500. both the tree and the enemy would take 10 damage from the player attack. in the case of the tree it will hurt it a little but the player wont gather anything. if the player performs a gather on the tree however, the igatherable will pass through the damage stored in the decay( maybe 125) but they get to gather resources.

i don’t even use (open world)attacks in my game, the combat is turn based, but i wanted to build it out so i could use the system in more than one game. I have never tweaked with the numbers of the melee damage as it never mattered to me. I wrote the script when i was doing the 2dARPG course so i tried to write it so it could be used as a more " chill gathering system" or within an ARPG.

funnily enough, even my script uses the health for both the player and the enemies, BUT… My health script is a little bit way too advanced beyond the course, because it has connections to a skilling system that makes things a bit more complex, so breaking it down into subscripts that follow a line of inheritance can be a bit complex for me. @bixarrio said that earlier, and I’d love to actually refactor the script, but this can cause unwanted issues for me, so unless Brian gives the greenlight, I’d personally stick to what I currently have :slight_smile:

In my previous version of the game, the lifetime of an item was based on how many of the item quantity were available, and each time you strike a resource item, based on your level, you can get 1-2 logs, and the lifetime was randomized for each respawn… it was a fun little system

I’d love it back, but I think I’ll give this a try at the end, after we are done with the sophisticated systems :slight_smile:

If I’m being brutally honest, the new attack system Brian designed sounds very promising. I can’t wait to see what the new enemies will be capable of doing!

turn-based as in, a turn based game, or turn-based as in, each enemy will wait for his turn to try and attack you?

because it has connections to a skilling system that makes things a bit more complex, so breaking it down into subscripts that follow a line of inheritance can be a bit complex for me.

Im working on this now. mines going well but i diagramed it out months ago in preperation. still a challenge. it sounds like you overloaded your health script and need a big refactor.

turn-based as in, a turn based game, or turn-based as in, each enemy will wait for his turn to try and attack you?

turn based combat like golden era Final Fantasy

trust me, I know… Wasn’t expecting to need that script out of there anytime soon lel

just like you, I started to try and make a RuneScape lookalike work… then third person got tempting lel

The class is a GatherableItem. Calling the prefab gatherableItem could get confusing down the line. This should be more like our WeaponModel in the WeaponConfig… My first look at the screenshot left me briefly confused on that. You might consider renaming this field to something like gatherableModel.

SpawnMethod (really can just be Spawn(), we know it’s a method because it has () on it) needs to take in a transform, rather than a Vector3. It also needs to be public or you won’t be able to call it.

public GameObject Spawn(Transform transform)
{
    var spawnedItem = Instantiate(gatherableItem, transform);
    return spawnedItem;
}

Other than that, we’re on the right track…

So next we need something similar to a PickupSpawner to actually spawn in a resource.

Take a look at PickupSpawner.cs. This will be the starting point for our GatherableItemSpawner.cs

For now the script will need:

  • A [SerializedField] of a GatherableItem
  • In Awake(), the GatherableItem’s prefab should be spawned by calling the GatherableItem’s Spawn(). Pass the transform to Spawn.
  • The Test: Place a GatherableItemSpawner on an Empty Gameobject, assign it a GatherableItem and place it within a scene. Run the scene. The prefab you assigned to the GatherableItem scriptableObject should now be spawned within the scene.

Sounds fairly simple (hopefully it is), will give it a go in a bit and keep you updated on my progress over the next comment :grinning_face_with_smiling_eyes:

Before we start, is it safe for me to delete the previous Resource Gathering scripts? I just don’t want to clog my project up any more than that, it’s already quite cloggy :confused:

lol I’m not sure what in the world have I done wrong there, but for some reason, the ‘Spawn()’ argument (P.S: I re-named that to ‘SpawnGatherableItem()’) was rejecting Transform inputs into the ‘Instantiate()’ function, and now it works perfectly fine… I think there is multiple Instantiation functions, and I happened to use the wrong one… (it’s something I became aware of in the Parkour course I did, not-so-long ago)

Anyway, the second challenge works, as follows:

  1. The ‘GatherableItem.cs’, after a bit of variable renaming:
using GameDevTV.Inventories;
using UnityEngine;

[CreateAssetMenu(fileName = "new Gatherable Item", menuName = "RPG/Inventory/Gatherable Item")]

public class GatherableItem : InventoryItem
{
    [SerializeField] GameObject gatherableModel;


    public GameObject SpawnGatherableItem(Transform spawnPoint)
    {
        var spawnedItem = Instantiate(gatherableModel, spawnPoint);
        return spawnedItem;
    }
}
  1. ‘GatherableItemSpawner.cs’:
using UnityEngine;

public class GatherableItemSpawner : MonoBehaviour
{
    [SerializeField] GatherableItem itemToSpawn;

    void Awake() 
    {
        itemToSpawn.SpawnGatherableItem(transform);
    }
}
  1. The in-game engine screenshot:

in the meanwhile I’ll probably go check the ‘EnemyAttackingState.cs’, I’ve been low-key waiting for this one for a while (and from the get-go, just going to let you know that the lecture number is 34 for the attacking state, not 33 :laughing:)

Edit: The new attacking state works perfectly, but it’s… clearly incomplete lel, as we don’t respawn to the proper spawn point, and things are a bit messy right now lel (too messy to explain, so I’ll wait till the end of this expected-to-be-incredibly-lengthy-series-which-I-asked-for, before I say exactly what’s wrong :slight_smile:)

Ok, now I’m confused again…
First, I thought that you had existing working resource gathering scripts, and that all you needed was to interface with them…
Then it looked like you were saying you had no scripts…
Now you do have scripts…

The scripts were created for the point and click system. The third person one is a more complex one (current point-and-click functionalities are similar, but they’re greatly limited… I want to expand on that), so I figured I’d replace them :grimacing: - should I post them here?

Quite literally all of the scripts we’re working with were designed for point and click.

If you have a working gathering system, then it’s using an IHandleRaycast to interface with the point and click. All that’s needed then is to call the same entry points that the IHandleRaycast calls when it determines you can do the operation.

That was sort of the whole point of doing all of the different interactions one by one in the tutorial, to show you how this is done.

If I’d known you did have scripts after all, I wouldn’t be going through these steps.

Post your existing scripts and I’ll have a look at them, but likely tomorrow, as it’s about to be date night.

sure, here you go (P.S: the dead code is either comments, or was mathematical experiments for me to determine how I want this to go):

Here is my first one, ‘ResourceGathering.cs’ (which is placed on the resource source, like a tree or something):

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

// 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, ITarget//, IRaycastable
    {
        [SerializeField] InventoryItem resourceToAccumulate;    // the loot we are getting from the tree, rock, or whatever we're harvesting
        [SerializeField] int maxResourcesPerHit;    // how many of that loot is being acquired, per mouse click
        public int quantityLeft;   // 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)
        
        internal 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;
        [SerializeField] public int minSkillRequired;
        [SerializeField] public int maxSkillRequired;

        SkillStore skillStore;

        private NavMeshAgent navMeshAgent;
        private int levelToUnlock;

        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>();
            isMovingTowardsResourceObject = false;
            resourceGatherer = playerInventory.GetComponent<ResourceGatherer>();
        }

        public int GetMaxSkillRequired() {
            return maxSkillRequired;
        }

        public int GetMinSkillRequired() {
            return minSkillRequired;
        }

        public int GetMaxResourcesPerHit() {
            return maxResourcesPerHit;
        }

        public int GetQuantityLeft() {
            return quantityLeft;
        }

        public InventoryItem ResourceToAccumulate() {
            return resourceToAccumulate;
        }

        public void DestroyResource() {
            Destroy(gameObject);
            isDestroyed = true;
        }

        public Skill GetAssociatedSkill() {
            return associatedSkill;
        }

        public int GetExperienceValue() {
            return experienceValue;
        }

        public void AccumulateXP()
        {
            skillExperience.GainExperience(associatedSkill, experienceValue);
        }

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

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

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

        public bool HandleRaycast(PlayerController callingController) {
                if (Input.GetMouseButtonDown(0)) {
                // if you don't have enough space, don't start resource gathering
                if(!Inventory.GetPlayerInventory().HasSpaceFor(resourceToAccumulate)) return false;
                // if you're not high enough of a level to touch a resource, or don't have the right weapon, don't start
                if (skillStore.GetSkillLevel(associatedSkill) < LevelToUnlock()) return false;  // place an 'or' ("||")statement later to count for holding a weapon associated to the skill (hatchet for trees, pickaxe for mining, etc), by checking the 'equipLocation.Weapon'
                // if all is well so far, start gathering the resources
                resourceGatherer.Gather(this);
                }
                return true;
        } */

    }

}

‘ResourceGatherer.cs’ (this one goes on the player, so he can interact with the environment):

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

    }

    private void TriggerGatheringAnimation() {

        if (GetAssociatedSkill() == Skill.Woodcutting) {
        
        playerInventory.GetComponent<Animator>().ResetTrigger("stopCuttingWood");
        playerInventory.GetComponent<Animator>().SetTrigger("cutWood");

        }

        else if (GetAssociatedSkill() == Skill.Mining) {

            playerInventory.GetComponent<Animator>().ResetTrigger("stopMiningRocks");
            playerInventory.GetComponent<Animator>().SetTrigger("mineRocks");

        }

        // you get the drill for the rest...

    }

    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() {

        // Using the 'LookAt()' we invented below, instead of 'transform.LookAt()',
        // to avoid the character from sloping his y-axis values, the 'Michael Jackson' effect:
        LookAt(target.transform.position);
        /* if (timeSinceLastGather < timeBetweenGathers) return;
        TriggerGatheringAnimation();
        Gather();
        timeSinceLastGather = 0f; */

        if (timeSinceLastGather > timeBetweenGathers) {
            TriggerGatheringAnimation();
            Gather();
            timeSinceLastGather = 0;
        }

    }

    void LookAt(Vector3 position) {
        position.y = transform.position.y;
        transform.LookAt(position);
    }

    public void Gather() {

        // Steps for the 'Gather()' function:
        // 1. If your source is not out of resources yet, then start off by playing the 'gathering' animation
        // 2. get the players' Skill level for the skill (Woodcutting, Mining, Farming, etc)
        // 3. Get the probability of the player getting 'maxResourcesPerHit', each time he strikes the resource (the higher the level, the closer the player gets to 'maxResourcePerHit')
        // 4. If the player gets a resource, determine (out of a max value of 'maxResourcesPerHit') how many resources he can get, based on his resource-gathering level
        // 5. ensure that the player does NOT get more than the resources left in the source (so he can't get 6 logs if the tree only has 2 more left for example)
        // 6. Give the player the resource, deduct it from the source, and then give him XP for it
        // 7. If the source is out of resources, kill it and stop the 'gathering' animation

        if (target.quantityLeft > 0) {
            TriggerGatheringAnimation();
            var playerSkillLevel = skillStore.GetSkillLevel(target.GetAssociatedSkill());
            var playerDropChance = Mathf.Clamp01((playerSkillLevel - target.minSkillRequired)/(float)(target.maxSkillRequired - target.minSkillRequired));

            if (Random.value <= playerDropChance) {
            int amountToGive = DetermineAmountToGive(playerSkillLevel, target.GetMinSkillRequired(), target.GetMaxSkillRequired(), target.GetMaxResourcesPerHit());
            amountToGive = Mathf.Min(amountToGive, target.GetQuantityLeft());
            playerInventory.AddToFirstEmptySlot(target.ResourceToAccumulate(), amountToGive);
            target.quantityLeft -= amountToGive;
            AccumulateXP();
            }
        }

        if (target.quantityLeft <= 0) {
            target.DestroyResource();
            StopGatheringAnimation();
        }

    }

    private int DetermineAmountToGive(int playerSkillLevel, int minLevel, int maxLevel, int maxResourcesPerHit) {

        // float randomValue = Random.value * (playerSkillLevel - minLevel);
        float randomValue = Mathf.CeilToInt(Random.value * (playerSkillLevel - minLevel));
        float fractionPerLevel = maxResourcesPerHit / (maxLevel - minLevel);
        float amountToGive = 1f;

        // for (int i = 0; i < (maxLevel - minLevel); i++) {

            // changing 'randomValue -= i' to 'randomValue -= 1' below 
            // ensures that the "CHANCES" of us getting more than a log at a time at higher
            // levels are properly calculated:
        //    randomValue -= 1;   

        //    amountToGive += fractionPerLevel;
                amountToGive += randomValue * fractionPerLevel;
        // if (randomValue <= 0f) break;
        // }
            // return Mathf.FloorToInt(amountToGive);
            return Mathf.FloorToInt(Mathf.Min(amountToGive, maxResourcesPerHit));
        }

    

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

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

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

    private void StopGatheringAnimation() {
        
        if (GetAssociatedSkill() == Skill.Woodcutting) {
        
        GetComponent<Animator>().ResetTrigger("cutWood");
        GetComponent<Animator>().SetTrigger("stopCuttingWood");
        
        }

        else if (GetAssociatedSkill() == Skill.Mining) {

        GetComponent<Animator>().ResetTrigger("mineRocks");
        GetComponent<Animator>().SetTrigger("stopMiningRocks");

        }

        // you get the drill for the rest...

    }

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

}

‘ResourceRespawner.cs’ (this one goes on the parent of the resource, to ensure it waits for some time and respawns, and assigns values accordingly. This one is a major headache with the NavMesh Agent, mainly because everytime I bake it, I have to deactivate and re-activate the children trees):

using System.Collections;
using UnityEngine;

namespace RPG.ResourceManager
{
    public class ResourceRespawner : MonoBehaviour
    {
        [SerializeField] GameObject resourceToRespawn;
        [SerializeField] int hideTime;
        [SerializeField] int originalQuantity = 10;

        private GameObject resourceObject;
        private ResourceGathering resourceGathering;
        private bool isRespawning = false;

        void Start()
        {
            // Initialize the resourceGathering component
            resourceGathering = GetComponent<ResourceGathering>();

            // Spawn the initial tree
            SpawnSource();
        }

        void Update()
        {
            // Check if the current tree is destroyed and respawn it
            if (resourceObject == null && !isRespawning)
            {
                StartCoroutine(RespawnAfterDelay());
            }
        }

        IEnumerator RespawnAfterDelay()
        {
            isRespawning = true;
            yield return new WaitForSeconds(hideTime);
            SpawnSource();
            isRespawning = false;
        }

        private void SpawnSource()
        {
            
            // Instantiate a new tree ('Quaternion.identity' = no Rotation assigned - Success):
            resourceObject = Instantiate(resourceToRespawn, transform.position, Quaternion.identity);

            // Make the current tree a child of this GameObject (the Tree Respawner - Success):
            resourceObject.transform.parent = transform;

            // Assign a tag to the spawned resource source - NOT NECESSARY THOUGH (Success):
            // resourceObject.gameObject.tag = "ResourceTree";

            // Access the ResourceGathering script on the instantiated object and set its properties (Success):
            ResourceGathering resourceGathering = resourceObject.GetComponent<ResourceGathering>();

            // If you have a new source for a specific resource, reset the values for another round of resource gathering:
            if (resourceGathering != null)
            {
                // Reset the quantity left for resource gathering
                resourceGathering.quantityLeft = originalQuantity;  // Reset the quantity for the next resource instance
                resourceGathering.isDestroyed = false; // Reset the IsDestroyed flag
            }
        }
    }
}

‘ResourceDataStructure.cs’, which only helps us create Resource Items in the game engine:

using UnityEngine;
using GameDevTV.Inventories;

[CreateAssetMenu(fileName = "Resource Data", menuName = "Resources", order = 0)]
public class ResourceDataStructure : InventoryItem
{
    
    // Creating a Resource to gather as an Inventory Item, nothing more

}

and ‘ResourceFinder.cs’, which was supposed to be my first step to the new third person integration:

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

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

namespace RPG.ResourceManager {

public class ResourceFinder : RangeFinder<ResourceGathering> {}

}

(P.S: I am really sorry for the huge confusion)

lol I’m probably not having any of these anytime soon :sweat_smile: - but hey, enjoy your night :slight_smile:

Edit: I think Brian is trying to break the ‘longest date night ever’ record… :stuck_out_tongue:

Yesterday was a not great day, sorry about the delay.

This is actually a lot simpler than you think it will be…
This section here in your ResourceManager’s HandleRaycast method is effectively your IsValid() method for the ITarget interface.

So our character is already close enough to the resource to mine/herbalize/pump from a well/whatever it needs to be. This means that we don’t have to worry about any of the code for moving to the resource because you’re there. All we have to do is worry about facing the target. Fortunately, we have a State for that, the PlayerFacingState.

As such, we don’t need to worry about the Update() method at all. It can go away.

We’ve already gathered as well, so we don’t need to worry about LookAt(target.transform.position);

I think for your PlayerGatheringState(), you need to know the name of the animation that’s appropriate… so rather than a TriggerGatheringAnimation, I would have a method exposing the animaton state name

public string GetGatheringAnimation()
{
    switch(GetAssociatedSkill())
    {
          case: Skill.WoodCutting : return "CutWood"; //Make sure you use the name of the state for this
          case: Skill.Mining: return "Mine";
          default: return "Pickup";
    }
    return "Pickup";
}

For optimum visual impact, you should have an animation event on your gathering animations… for consistency, I would use PickupItem, since we’ve already set up the AnimationEventRelay script to listen for PickupItem() and relay that into a PickupItemEvent which your PlayerGathering state can subscribe to when it is active.
Then, when PickupItemEvent is triggered, you can call GatherBehaviour on the ResourceGatherer. You’ll likely have to pass it the target, as we’re bypassing passing GatherBehaviour the target now, and that will be used to set target.
At the end of Gather(), you can remove the StopGatheringAnimation() method.

Put the Pickup tag on all of your Gathering animations, which you can check for when they end and return to PlayerFreeLookState().

lol all is good, I just figured I’d tease you a bit about being gone for a day when I said you’re trying to break the longest date night record :laughing:

Anyway, so here’s what I understood so far:

  1. eliminate the ‘ResourceGatherer.Update()’ function
  2. eliminate the ‘LookAt()’ custom function we created in ‘ResourceGatherer.cs’
  3. Rewire the ‘TriggerGatheringAnimation()’ function (and probably the same for the stopping gathering animation function, just taking a wild guess here)
  4. Set the animation tags to “Pickup”, and rename them to whatever you called them in the “GetGatheringAnimation()” function

Did I miss out on anything…?

I didn’t catch that… Do I uncomment the handling of the Raycast though? Genuinely concerned if this is even supposed to happen or not

I didn’t fully understand that… Do I add anything or is this already setup by me? Not for the tags, I’m asking about transitioning between State Machines (P.S: I’m a bit of a confused person rn, just trying to survive this wave lel…)

And finally, I need to set a button up in the Input Action Map, right? I’m surprised we didn’t address that…

Rewire == delete? Yes. There is no need for a stopping gathering animation function.

Just copy that logic into IsValid(). The rest of HandleRaycast is not useful for this.

Take a look at PlayerPickupState(). Pretty much the same logic in PlayerPickupState() will be used in PlayerGatheringState(). Big difference is getting the Animation name from GetGatheringAnimation(), and then calling the ResourceGatherer’s GatherBehaviour instead of Pickup.

Don’t forget the ResourceGatherer doesn’t have a target set, so add a parameter to GatherBehaviour with the target and set this.target=target;

so this function in ‘ResourceGatherer.cs’ can go? You mentioned there’s no need for a ‘StopGatheringAnimation()’ function, and this one relies on it, soo…:

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

Yes, though you might notice an issue if you delete it… I’ll let you figure out how to solve that…

oh I know what the issue is, hence why I’m asking :stuck_out_tongue_winking_eye:

Before we begin, I have to admit: I still don’t understand what an “Animation Event Relay” is…

[SKIP TO EDIT 3]

OK so… there are a few major changes happening here (pardon me if it feels like I’m sloppy, it takes me time to be able to catch up and think again after long breaks from the project… by long, I mean anything over 6 hours):

In ‘AnimationEventRelay.cs’, I added this event:

        public event System.Action GatherItemEvent;

void GatherItem() 
        {
            GatherItemEvent?.Invoke();
        }

and then I created the ‘PlayerGatheringState.cs’ script, which I’m assuming is called from ‘PlayerFreeLookState.cs’:

using RPG.ResourceManager;
using RPG.States.Player;
using UnityEngine;

public class PlayerGatheringState : PlayerBaseState
{

    private ResourceGatherer target;
    private Vector3 position;

    public PlayerGatheringState(PlayerStateMachine stateMachine, ResourceGatherer target) : base(stateMachine)
    {
        this.target = target;
    }

    public override void Enter()
    {
        if (target == null) 
        {
            stateMachine.SwitchState(new PlayerFreeLookState(stateMachine));
            return;
        }

        position = target.transform.position;
        stateMachine.Animator.CrossFadeInFixedTime(target.GetGatheringAnimation(), AnimatorDampTime);
        stateMachine.AnimationEventRelay.GatherItemEvent += AnimationEventRelay_HandleResourceGathering;

    }

    public override void Tick(float deltaTime)
    {
        FaceTarget(position, deltaTime);
        Move(deltaTime);
        if (GetNormalizedTime(target.GetGatheringAnimation()) > 0.80f) 
        {
            stateMachine.SwitchState(new PlayerFreeLookState(stateMachine));
        }
    }

    public override void Exit()
    {
        stateMachine.AnimationEventRelay.GatherItemEvent -= AnimationEventRelay_HandleResourceGathering;
    }

    void AnimationEventRelay_HandleResourceGathering() 
    {
        target.Gather();
    }
}

and since that function needs to be called from somewhere, I figured I’d add this in ‘PlayerFreeLookState.cs’:

// in 'Enter()':
            stateMachine.InputReader.InteractWithResourceEvent += InputReader_HandleResourceGatheringEvent;

// in 'Exit()':
            stateMachine.InputReader.InteractWithResourceEvent -= InputReader_HandleResourceGatheringEvent;

// new function (MAJOR PROBLEM HERE - more details below):
        private void InputReader_HandleResourceGatheringEvent() 
        {
            if (stateMachine.ResourceFinder.FindNearestTarget()) 
            {
                PlayerGatheringState nextState = new PlayerGatheringState(stateMachine, stateMachine.ResourceFinder.CurrentTarget);
                stateMachine.SwitchState(new PlayerFacingState(stateMachine, stateMachine.ResourceFinder.CurrentTarget.transform.position, nextState));
            }
        }

// in 'HandleAttackButtonPressed():
if (stateMachine.ResourceFinder.HasTargets) 
            {
                InputReader_HandleResourceGatheringEvent();
                if (stateMachine.ResourceFinder.HasTargets) return;
            }

however, I have one major issue with my ‘PlayerGatheringState.cs’ script because of this… I was confused for a bit about whether ‘target’ should be a ‘ResourceGatherer.cs’ or a ‘ResourceGathering.cs’ script reference (it makes perfect sense to use ‘ResourceGathering.cs’), mainly because ‘GetGatheringAnimation()’ and ‘Gather()’ are in ‘ResourceGatherer.cs’ instead of ‘ResourceGathering.cs’. Do I just copy paste the functions, or…? I’m sure some refactoring will need to be done, right? (Until this is fixed, I can’t run and test the game :sweat_smile:)

Edit: Not sure if that fixes the issue or not, but I added a third input to the stateMachine of ‘PlayerGatheringState()’, so now it looks like this:

private ResourceGatherer player;

public PlayerGatheringState(PlayerStateMachine stateMachine, ResourceGathering target, ResourceGatherer player) : base(stateMachine)
    {
        this.target = target;
        this.player = player;
    }

// changes in "AnimationEventRelay_HandleResourceGathering()":

// target.Gather();
player.Gather();

so now I have a reference to the player, hence can call the ‘Gather()’ and ‘GetGatheringAnimation()’ methods… still trying to figure out how to call that from ‘PlayerFreeLookMachine.cs’

Edit 2: Figured out how to call it, so now the call to ‘PlayerGatheringState()’ looks as follows:

        private void InputReader_HandleResourceGatheringEvent() 
        {
            if (stateMachine.ResourceFinder.FindNearestTarget()) 
            {
                PlayerGatheringState nextState = new PlayerGatheringState(stateMachine, stateMachine.ResourceFinder.CurrentTarget, stateMachine.GetComponent<ResourceGatherer>());
                stateMachine.SwitchState(new PlayerFacingState(stateMachine, stateMachine.ResourceFinder.CurrentTarget.transform.position, nextState));
            }
        }

HOWEVER, It’s still not working… As in, I can get close to a rock or something, but can’t interact with it. I can’t gather resources, or play the animations. Did I miss out on something?

And I added a reference to the ResourceFinder in ‘PlayerStateMachine.cs’

Edit 3: the issue was that my ‘ResourceGathering.cs’ script was not attached to the source (i.e: rock, tree, etc…). I figured that out by accident, and now all works as expected. HOWEVER, THERE ARE TWO MAJOR PROBLEMS (and I’m guessing this has to do with the ‘Cancel()’ we spoke of earlier):

[UNSOLVED]

  1. The player refuses to return to freeLookState, so he’ll keep playing the animation forever with no exit modes (and no, I did not delete the ‘Cancel()’ function). The only way out of it, is to shut the entire game down (by hitting the pause button)

[SOLVED the second problem: Don’t laugh, but… the entire time, I literally forgot to hit ‘Apply’ on the new animation event. That’s why you don’t try multitasking at work :zipper_mouth_face:]

  1. No logs are being gathered. He’s just playing the animation, but no resource gathering or XP Gain is happening for some reason (I checked my mathematical formula, all seems fine there for now). I thought adding an event “GatherItem()”, the function I created in ‘AnimationEventRelay.cs’ into the animation was going to help, but it did not…

[NEW PROBLEM, the result of solving the second one]
3. When the tree/rock is dead, as long as its dead, the player will keep playing the animation and push out this error:

MissingReferenceException: The object of type 'ResourceGathering' has been destroyed but you are still trying to access it.
Your script should either check if it is null or you should not destroy the object.
RPG.ResourceManager.ResourceGathering.DestroyResource () (at Assets/Project Backup/Scripts/ResourceManager/Old Scripts/ResourceGathering.cs:86)
RPG.ResourceManager.ResourceGatherer.Gather () (at Assets/Project Backup/Scripts/ResourceManager/Old Scripts/ResourceGatherer.cs:136)
PlayerGatheringState.AnimationEventRelay_HandleResourceGathering () (at Assets/Project Backup/Scripts/State Machines/Player/PlayerGatheringState.cs:47)
RPG.Control.AnimationEventRelay.GatherItem () (at Assets/Project Backup/Scripts/Control/AnimationEventRelay.cs:22)

and when it respawns, he’ll continue the gathering process (not just the animations, the entire process) like nothing ever happened, and like I mentioned in 1, there’s no way to stop it)

[REQUESTS]

  1. Additionally, how do you cut the player off playing the animation, if for example he wants to walk away from the tree at any moment? Preferably if he pressed the interaction button (and the attack button we assigned to handle everyone together) on the action map… This one is crucial for me to know

  2. Finally, how do we make it click-dependent. I want him to only play the gathering process when we click/hold the mouse click down (or whatever triggers it in this case), rather than going on forever and boring the player out… something like we did with the attacking, where transitioning to the next animation only happens when our mouse is clicked down for longer than a specific duration of the animation, and cuts off the sequence if we don’t click for long enough

I saw your edit.
With the TimeKeeper you don’t need anything else for the time. It has no connection to the crafting system. With the changes we made to your version it just returns the number of seconds that has passed since 1 January 2023. When the resource is destroyed you grab those seconds (let’s call it startSeconds), and you can get currentSeconds from the TimeKeeper on some interval and check if the currentSeconds - startSeconds is greater or equal to the time it will take for a resource to respawn. The resource spawner should save the startSeconds to the save file so it wouldn’t matter if the game is running or not.

You’d also do the check when RestoreState(...) is called, because the time could have passed while the game is not running and on load you’d then want to immediately spawn the resource.

Privacy & Terms