Resource Gathering for Third person

Ignore that comment. It’s already solved… I coded a bunch of extra systems that didn’t work, and after that I found out it wasn’t working because of only one reason. I had the wrong tag on my animations, and because I used a new gathering system (frankly speaking, I didn’t understand much what we had to do to the pickup system to use it for gathering as well) instead of the pickup system, the pickup tag on my animations messed with my tick function for a while…

You honestly don’t have to. I understand it can get complex, but I’m also trying my hardest with my scripts to get things working on my own, based on my limited knowledge (this has always been the case, since day one. I post a question, and try solve it before you see it, and if I manage I let you know)

I mean… I would try follow along, but for now if it works, don’t touch it :sweat_smile: (unless there’s a bigger advantage with yours, I’d prefer to trust myself on mine for now :laughing:). Extra scripts are great for my knowledge though, I learned so much by exploring many unknown areas here, and I’m down for more

And that’s what I’m trying to figure out, because for the point and click system this was not an issue, somehow… What exactly is going on when the item gets destroyed and respawned, that can potentially lead to this glitch, is what I’m trying to figure out

If it helps, there’s an NRE it gives out, as follows:

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.Core.RangeFinder`1[T].FindNearestTarget () (at Assets/Project Backup/Scripts/Core/RangeFinder.cs:50)
RPG.States.Player.PlayerFreeLookState.InputReader_HandleResourceGatheringEvent () (at Assets/Project Backup/Scripts/State Machines/Player/PlayerFreeLookState.cs:136)
RPG.InputReading.InputReader.OnInteractWithResource (UnityEngine.InputSystem.InputAction+CallbackContext context) (at Assets/Project Backup/Scripts/Input Controls/InputReader.cs:176)
UnityEngine.InputSystem.Utilities.DelegateHelpers.InvokeCallbacksSafe[TValue] (UnityEngine.InputSystem.Utilities.CallbackArray`1[System.Action`1[TValue]]& callbacks, TValue argument, System.String callbackName, System.Object context) (at Library/PackageCache/com.unity.inputsystem@1.7.0/InputSystem/Utilities/DelegateHelpers.cs:46)
UnityEngine.InputSystem.LowLevel.<>c__DisplayClass7_0:<set_onUpdate>b__0(NativeInputUpdateType, NativeInputEventBuffer*)
UnityEngineInternal.Input.NativeInputSystem:NotifyUpdate(NativeInputUpdateType, IntPtr)

This error both comes out when the item just got destroyed, and when it respawns

Here’s (roughly) what happens in the PickupSpawner:

  • The pickup is spawned. The player picks it up, so it is destroyed. Job’s done.
  • When it’s being saved, the saving system stores whether or not this pickup was picked up.
  • When it is being loaded, the spawner checks the saved state
    • If it was picked up but it’s currently still there, the spawner destroys it.
    • If it wasn’t picked up but it’s currently not there, the spawner spawns it.

Here’s what’s different in your tree: you have 2 things to spawn - a tree and a stump
Here’s (roughly) what should happen in your TreeSpawner:

  • The tree is spawned. The player chops it down, so it is destroyed. You spawn the stump. Grab the global seconds. Job’s done.
  • When it’s being saved, the saving system stores whether or not this tree was chopped down.
  • When it is being loaded, the spawner checks the saved state
    • If it was chopped down but it’s currently still there, the spawner destroys it and spawns a stump.
    • If it wasn’t chopped down but it’s currently a stump, the spawner destroys the stump and spawns the tree.

Along with it, you will store the global seconds from the TimeKeeper taken when the tree was chopped down. Now when you load the spawner from a save, you can see when it was chopped down, and you can determine if it should still be chopped down. If it was chopped down 32 hours ago but it takes 24 hours to respawn, it should be a tree again. If it’s a tree, do nothing. If it’s a stump, destroy the stump and spawn a tree.


You are doing too many things at once. Brian is trying to help you get the gathering state working, and you are trying to do all the other things at the same time. Stop. Get the gathering working first. You can worry about the other things later

Don’t worry, I didn’t even start trying to save the resource gathering system state, because… well… detecting a respawned gameObject is not working for some reason, and I’m still trying to figure out why (at least it’s not buggy animations anymore :sweat_smile: - by far the most ridiculous bug fix to me, comparing how much work I tried before fixing it)

by the way, if it helps in anyway, this is my ‘ResourceRespawner.cs’ script, which is responsible for respawning stuff (and I’m guessing where ‘IsValid()’ also comes into play). To some extent, I also think ‘IsValid()’ also should’ve referred to something here to get this to work:

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

My main problem here is, they are respawnables. For some reason, once a Respawnable is dead, this error just becomes a permanent error in the game, and I can’t wrap my head around it (in simpler terms, anytime I press the action map button of resource gathering, this error shows up, whether I’m near a resource or far away from it…):

And when I click on them, this is what I get from the first one:

MissingReferenceException while executing 'performed' callbacks of 'Player/InteractWithResource[/Keyboard/u]'
UnityEngine.InputSystem.LowLevel.NativeInputRuntime/<>c__DisplayClass7_0:<set_onUpdate>b__0 (UnityEngineInternal.Input.NativeInputUpdateType,UnityEngineInternal.Input.NativeInputEventBuffer*)
UnityEngineInternal.Input.NativeInputSystem:NotifyUpdate (UnityEngineInternal.Input.NativeInputUpdateType,intptr

and the second one:

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.Core.RangeFinder`1[T].FindNearestTarget () (at Assets/Project Backup/Scripts/Core/RangeFinder.cs:50)
RPG.States.Player.PlayerFreeLookState.InputReader_HandleResourceGatheringEvent () (at Assets/Project Backup/Scripts/State Machines/Player/PlayerFreeLookState.cs:136)
RPG.InputReading.InputReader.OnInteractWithResource (UnityEngine.InputSystem.InputAction+CallbackContext context) (at Assets/Project Backup/Scripts/Input Controls/InputReader.cs:176)
UnityEngine.InputSystem.Utilities.DelegateHelpers.InvokeCallbacksSafe[TValue] (UnityEngine.InputSystem.Utilities.CallbackArray`1[System.Action`1[TValue]]& callbacks, TValue argument, System.String callbackName, System.Object context) (at Library/PackageCache/com.unity.inputsystem@1.7.0/InputSystem/Utilities/DelegateHelpers.cs:46)
UnityEngine.InputSystem.LowLevel.<>c__DisplayClass7_0:<set_onUpdate>b__0(NativeInputUpdateType, NativeInputEventBuffer*)
UnityEngineInternal.Input.NativeInputSystem:NotifyUpdate(NativeInputUpdateType, IntPtr)

and since we’re at it, here is my current ‘ResourceGathering.ITarget.IsValid()’ function:

        bool ITarget.IsValid()
        {
            if (!Inventory.GetPlayerInventory().HasSpaceFor(resourceToAccumulate)) return false;
            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'
            // inject code here that checks if EquipLocation.Weapon has the required axe (level-based check) or not
            resourceGatherer.Gather(this);
            return true;
        }

and there’s no way you can harvest them ever again after that, and it’s driving me nuts, as in WHY can I harvest the first one, but not the second time onwards, although EVERYTHING is exactly the same in terms of settings…?

MORE IMPORTANTLY, WHY CAN’T I HARVEST LITERALLY ANYTHING AT ALL IF ONE RESOURCE IS DESTROYED (I’m not angry, just highlighting an extremely important issue)

At this point in time, I’m convinced that there must be a setting of some sort that tells the RangeFinder code that this item is destroyed, but that doesn’t mean it should permanently stop searching for new ‘ResourceGathering.cs’ scripts (it’s like someone who isn’t over their ex yet, and won’t date other girls)… I think I can copy something from the Pickups regarding that, as it doesn’t seem to have the same problem, just not sure what

Edit: 9 hours later, still can’t figure out what in the world is going on here…

Note that earlier, you only said it wasn’t working, you never mentioned that you had multiple errors. Errors are GOLD for figuring out what is going on
Two reasons:

  • You appear to be subscribing to an event without unsubscribing from it. Unfortunately, these are very hard to debug because they seldom yield a line number or even script… Oddly enough, you appear to be subscribing to the raw input event instead of setting up an event System.Action and calling that. Check each of your states and make sure that any subscriptions you have in Enter has a corresponding unsubscription in Exit.
  • You’re not informing the ResourceFinder that the ResourceGathering has been destroyed. I’m going to point you back to how we handle the Pickups… Review the lesson on pickups, and look at my notes about PickupTarget.cs and PickupFinder.cs. You’ll note that PickupFinder subscribes to an event added to PickupTarget.cs that should be called before the Pickup is picked up. Same logic applies to the ResourceGathering/ResourceFinder… Before you Destroy the ResourceGathering, you need to instruct the ResourceFinder to remove the Target. The error message is saying that the target isn’t there anymore, but it’s still in the list, hence it’s a MissingResourceException

You can go through the previous edits if it helps (Edit 6 and before is where the old major changes happened. Edit number 6 contains everything you’ll probably need, I just didn’t want to clutter this comment and overload you), but from try and error, this is where I noticed my major problem is, through debugging:

How do I do this, without completely screwing the systems connected to ‘RangeFinder.cs’? As far as I know, I can change the protection level from ‘protected’ to public (and trust me, I know exactly how dangerous this is!), for both the range finder and all the other components attached to it, and that would solve the problem, but I don’t think this is the ideal method

I know where my resources spawn and get destroyed, but I also want to know how to mark them as such to my range finder, this is currently what’s causing me the major headache (is that the system does not remove them on destruction. They get added, but never get removed for some reason… and comparing this to the Pickup state just baffles me more because some functions in there are not in ‘ResourceGathering.cs’)

and adding this block in ‘RangeFinder.OnTriggerEnter()’ did not work/help either…:

            if (other.IsDestroyed()) 
            {
                RemoveTarget(target);
                Debug.Log("Removing target, because it is destroyed...");
            }

This is in ResourceGathering.cs

public System.Action OnResourceDestroyed;

public void DestroyResource()
{
     OnResourceDestroyed?.Invoke();
     isDestroyed=true;
     Destroy(gameObject);
}

Your Resource finder will subscribe to the OnResourceDestroyed event

        protected override void AddTarget(ResourceGathering target)
        {
            base.AddTarget(target);
            target.OnResourceDestroyed += RemoveTarget;
        }

        protected override void RemoveTarget(ResourceGathering target)
        {
            base.RemoveTarget(target);
            target.OnResourceDestroyed -= RemoveTarget;
        }
    }

AND… THAT FIXED IT, THANK YOU BRIAN!

I honestly want to celebrate this one tbh :sweat_smile:

Anyway, I’ll go have a look at the respawn time saver, probably copy a bit of data from @bixarrio 's crafting system and go through the comments you both mentioned above, and if I get stuck I’ll keep you updated (not tonight though)

Thanks again Brian for bearing with me through this

I’m glad you’ve got it working. You may still wish to review the entire RangeFinding system. As you are trying to add more and more systems, a thorough understanding of the process is vital.

1 Like

Will do so, but knowing myself, I get a lot more out of understanding a system through try and error

So practicality is what makes things work for me, but I don’t mind going through it again today :slight_smile: (went through it, but I’ll probably do so again when I need it harder, xD)

OK so I’m not sure if what I attempted for this is right or wrong, but I gave it a shot anyway, although unfortunately it did not work as expected. Please have a look and let me know if this is right or wrong:

in ‘ResourceRespawner.cs’, I added the following:

private DateTime respawnTime;

// in 'SpawnSource()':
respawnTime = DateTime.Now.AddSeconds(hideTime);

// 'CaptureAsJToken()':

long binaryTime = respawnTime.ToBinary();
return JToken.FromObject(binaryTime);

// 'RestoreFromJToken(JToken state)':
long binaryTime = state.ToObject<long>();
respawnTime = DateTime.FromBinary(binaryTime);

so now the ‘ResourceRespawner.cs’ script looks like this:

using System;
using System.Collections;
using GameDevTV.Saving;
using Newtonsoft.Json.Linq;
using UnityEngine;

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

        internal GameObject resourceObject;
        private ResourceGathering resourceGathering;
        internal bool isRespawning = false;
        
        // (TEST)
        private DateTime respawnTime;   // new field for tracking respawn time

        void Start()
        {
            // Initialize the resourceGathering component
            resourceGathering = GetComponentInChildren<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
            }

            // (TEST) respawnTime = currentTime + hideTime
            respawnTime = DateTime.Now.AddSeconds(hideTime);
        }

        // (TEST Function)
        public JToken CaptureAsJToken()
        {
            // converting 'respawnTime' to a binary representation
            // (hint: anything NOT Serializable, like 'DateTime' must be converted to binary format prior to saving. and reversed when restoring (in 'RestoreFromJToken()')):
            long binaryTime = respawnTime.ToBinary();

            // Convert to JToken and return:
            return JToken.FromObject(binaryTime);
        }

        // (TEST Function)
        public void RestoreFromJToken(JToken state)
        {
            // Deserializing JToken to a long:
            long binaryTime = state.ToObject<long>();

            // get the DateTime value from a binary:
            respawnTime = DateTime.FromBinary(binaryTime);
        }
    }
}

Did I miss out on something…?! I tried placing the ‘respawnTime’ in ‘RespawnAfterDelay.WaitForSeconds()’ instead of ‘hideTime’, but the mismatching format was a mess, and casting it didn’t work either… (if I can fix it before you see this, I’ll delete this comment)

What does this mean?

Looking at your code, it’s not going to work. You store when the resource should respawn, but you completely ignore that and just set a delay for how long it takes. You say; “Resource should respawn after 30 minutes. That will be at ‘Now + 30 minutes’.” Then you stop the game, wait 20 minutes and run again. Now there’s 10 minutes before it should respawn, but you have a coroutine that’s going to wait 30 minute before it respawns the resource… (Edit: This was before your edited change)

Brian and I have given you 2 different methods of determining the respawn time. You don’t want a coroutine that waits to respawn it, unless this coroutine loops and checks on interval (see later when I do InvokeRepeating. It’s the same thing)

With Brian’s method, you just want to check if DateTime.Now is greater than the stored date (respawnTime).

private void Update()
{
    if (DateTime.Now >= respawnTime)
    {
        SpawnSource();
    }
}

With the TimeKeeper method, you just want to check if the difference between the stored time and now (as I mentioned in this post) is greater than hideTime.

private void Update()
{
    if (_timekeeper.GetGlobalTime() - storedRespawnTime > hideTime)
    {
        SpawnSource();
    }
}

It really doesn’t have to be in Update() though. You can decide on a ‘threshold’ - say 30 seconds - and do this on interval using InvokeRepeating()

private void CheckSpawnTime()
{
    if (WhicheverCheckYourGoingWith)
    {
        SpawnSource();
    }
}
private void Start()
{
    InvokeRepeating(nameof(CheckSpawnTime), 0, 30);
}

This will start immediately and only check if it should respawn every 30 seconds. Means it won’t spawn exactly when you expect it to, but within 30 seconds of that time.

As mentioned earlier, you can do this with your coroutine, but InvokeRepeating is already creating the same coroutine

IEnumerator CheckSpawnTime()
{
    var thirtySeconds = new WaitForSeconds(30);
    while (true)
    {
        yield return thirtySeconds;
        if (WhicheverCheckYourGoingWith)
        {
            SpawnSource();
            yield break;
        }
    }
}

It will run until you spawn a resource and then end. Next time the resource ‘dies’, you start it again.

Note None of this includes what to do with a save reload. It’s all just concepts and you will need to figure out how too fit this into your spawner


Edit

It won’t work, but it’s a step in the right direction. respawnTime is a DateTime and WaitForSeconds wants seconds. You’ll need to determine the remaining seconds if you want to go with that;

var remainingTime = (respawnTime - DateTime.Now).TotalSeconds;
yield return new WaitForSeconds(remainingTime);

But I’d recommend looking at the start of this post instead.

I’m reading through it all, and slowly trying to unconfuse myself, as I try to figure out alone what I should be doing. Please give me some time before the next update :slight_smile:

It’s not that confusing;
With Brian’s method it’s “Resource should spawn at 10am on Monday, 5 Feb 2024. Has it been 10am on Monday, 5 Feb 2024 yet?”
With my method it’s “Resource should spawn 8 hours after it’s been chopped down. Has it been 8 hours since it’s been chopped down yet?”

I think I’ll go with this one, especially that I already have a TimeKeeper, but fitting this all into a global timer, saving and restoring it is probably just another problem waiting to be solved (I’m confused why you didn’t address this in your earlier comment tbh)

not exactly 8 hours, preferably more like ‘hideTime’

OK I’m a little confused… what is ‘storedRespawnTime’ that you placed in ‘Update()’? At first I thought it was a rename of ‘hideTime’, but now I’m confused…:

        void Update()
        {
            // Check if the current tree is destroyed and respawn it
            if (resourceObject == null && !isRespawning)
            {
                // StartCoroutine(RespawnAfterDelay());
                if (timeKeeper.GetGlobalTime() - storedRespawnTime > hideTime) SpawnSource();
            }
        }

the compiler doesn’t recognize it

It was an example, just like ‘10am on Monday, 5 Feb 2024’. Of course it will be hideTime

No, everything is already there and I did mention all of this.

Here’s a breakdown;

  • When the resource is destroyed, you get _timeKeeper.GetGlobalTime(). This is the time when the resource died and this is what you will store in the save file
  • Now you start the coroutine I mentioned (let’s go with the coroutine since it’s less confusing to stop than InvokeRepeating)
  • When the resource respawns the coroutine will stop, and you can ignore the global time in the save file. You should have a flag to specify whether or not the resource is dead. This is in the PickupSpawner as shouldBeCollected

So, building off the PickupSpawner it may look like this (all happening on the fly, so no testing was done)

public class ResourceSpawner : MonoBehaviour, IJsonSaveable
{
    [SerializeField] GameObject resourceToSpawn;
    [SerializeField] int hideTime;
    
    private double destroyedTime;
    private GameObject resourceObject;
    private TimeKeeper timeKeeper;
    
    private void Awake()
    {
        timeKeeper = TimeKeeper.Get();
        SpawnResource();
    }
    
    public GameObject GetResource()
    {
        return resourceObject;
    }
    
    public bool IsCollected()
    {
        return GetResource() == null;
    }
    
    private void SpawnResource()
    {
        resourceObject = Instantiate(resourceToSpawn, transform);
    }
    
    private void DestroyResource()
    {
        var resourceToDestroy = GetResource();
        if (resourceToDestroy != null)
        {
            Destroy(resourceToDestroy);
            destroyedTime = timeKeeper.GetGlobalTime();
            StartCoroutine(CheckForRespawn());
        }
    }
    
    private IEnumerator CheckForRespawn()
    {
        var thirtySeconds = new WaitForSeconds(30);
        while (true)
        {
            yield return thirtySeconds;
            var currentTime = timeKeeper.GetGlobalTime();
            if (currentTime - destroyedTime >= hideTime)
            {
                SpawnResource();
                break;
            }
        }
    }
    
    public JToken CaptureAsJToken()
    {
        var data = new ResourceData(destroyedTime, IsColledted());
        return JToken.FromObject(data);
    }
    
    public void RestoreFromJToken(JToken state)
    {
        var data = state.ToObject<ResourceData>();
        destroyedTime = data.DestroyedTime;

        var shouldBeCollected = data.ShouldBeCollected;
        if (shouldBeCollected && !IsCollected())
        {
            // We don't use DestroyResource() here because we don't want to reset the destroyedTime
            Destroy(resourceObject);
            StartCoroutine(CheckForRespawn());
        }
        if (!shouldBeCollected && IsCollected())
        {
            SpawnResource();
        }
    }
}
[Serializable]
public struct ResourceData
{
    public double DestroyedTime;
    public bool ShouldBeCollected;
    
    public ResourceData(double destroyedTime, bool shouldBeCollected)
    {
        DestroyedTime = destroyedTime;
        ShouldBeCollected = shouldBeCollected;
    }
}

Again, I haven’t tested this. The DestroyResource() here is not used, but that’s because the code for destroying resources when it dies, etc. has not been added. This is just to get a resource to save and keep track of when it should spawn, and spawn when it should based on what the PickupSpawner is doing

I will, forever, be confused about how in the world do the both of you come up with incredibly complex code in a matter of minutes… Some of this stuff will probably take me months, if not years, to figure out

Anyway, I’ll give this a go in a bit, about an hour or so… I have to be elsewhere very soon

Just to be clear though, this is meant as a replacement for my ‘ResourceRespawner.cs’ script?

Yes, but also no. You have ResourceGathering stuff I don’t know anything about that you may still need. This here is just going to spawn an object at specific times. You will need to add the other stuff like deciding when the resource is depleted, etc. And later add the stump for destroyed trees

It was an example. It would be the time from the TimeKeeper when the resource was destroyed and saved in the save file, but because you don’t have any of that I just made it up.

ahh… the new script you wrote does not even get my resource object to show up. So basically, I can’t see the rock or the trees, none of that…

Not really that important, the Resource Respawner and the ResourceGathering.cs scripts are quite independent. Apart from just setting up a few public variables on respawn, there’s not much really else in common between them. I know that because, for the first time in my life, I am actually the original writer of that script :stuck_out_tongue_winking_eye:

If it helps though, this is my ‘ResourceGathering.cs’ script:

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] ResourceDataStructure 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;   // (OBSOLETE IN THIRD PERSON) 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;

        [Tooltip("Only change this value in the prefab if you want to see the effects in-game (between pauses only!)")]
        [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 event to fix the major issue of the system not being able to detect deleted resource sources
        // (Third Person Transition, for 'ResourceFinder.cs' to subscribe to)
        public event System.Action<ResourceGathering> OnResourceDestroyed;

        public void DestroyResource() {
            OnResourceDestroyed?.Invoke(this);
            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()
        {
            if (!Inventory.GetPlayerInventory().HasSpaceFor(resourceToAccumulate)) return false;
            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'
            // inject code here that checks if EquipLocation.Weapon has the required axe (level-based check) or not
            resourceGatherer.Gather(this);
            return true;
        }

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

    }

}

and the ‘ResourceGatherer.cs’ script:

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

namespace RPG.ResourceManager {

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 string GetGatheringAnimation() 
    {
        switch(GetAssociatedSkill()) 
        {
            case Skill.Woodcutting: return "cutWood";
            case Skill.Mining: return "mineRocks";
            // add more skills here, if necessary (fishing coming up soon...)
            default: return "Pickup";
        }
    }

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

    public 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) {
            GetGatheringAnimation();
            Gather();
            timeSinceLastGather = 0;
        }

    }

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

    /* // Test
    private ResourceFinder resourceFinder;

    private void Start() 
    {
        resourceFinder = GetComponentInChildren<ResourceFinder>();
    }
    // End of Test */

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

    }

    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()
    {
        // GetGatheringAnimation();
        target = null;
        GetComponent<Mover>().Cancel();
    }

}

}

and to keep your life simple, this is my original ‘ResourceRespawner.cs’ script (where I want to make the changes):

using System.Collections;
using UnityEngine;

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

        private GameObject resourceObject;
        private bool isRespawning = false;

        void Start()
        {
            // 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
            }
        }
    }
}

and then the ResourceDataStructure.cs script is an empty class that inherits from InventoryItem.cs, only so we can create resources to gather with

Brian, if you’re reading this, and you got confused somewhere, all we’re trying to do here is to save the timer for the resource respawners in a global timeKeeper format, using this script:


using System;
using UnityEngine;

namespace RPG {

    public class TimeKeeper : MonoBehaviour {

        private DateTime globalEpoch = new DateTime(2023,1,1);

        public static TimeKeeper GetTimeKeeper() {

            return FindObjectOfType<TimeKeeper>();

        }

        public double GetGlobalTime() {

            return (DateTime.Now - globalEpoch).TotalSeconds;

        }

    }

}

It’s something we created when making our Crafting System (Bixarrio helped me create it). The idea is to use this class to get the global computer time, and for the respawn times, we want to rely on that instead of the in-game engine timer, so long-time spawnables are not reliant on the gameplay timer, but it relies on the computer timer, which enables long-time respawns… that’s it

Privacy & Terms