Resource Gathering for Third person

STOP…
I’m on lunch and can only look over this briefly, but it appears you’re not fully understanding how we implemented the PickupFinder, and PickupState.

you are overcomplicating a simple solution. I’d review what I did for the PickupState…

  • Subscribed to a OnPickup event from InputReader
  • Checked the PickupFinder to see if there was a pickup nearby, which is automatically selected as the target
  • Created a PickupState with the target, but called the TurningState passing the call to PickupState as the 2nd parameter
  • In the PickupState, subscribed to the AnimationRelay event, and picked up the item when it was called.
  • Returned to the PlayerFreeLookState when animation was completed.

This is the model for how we’ll interface with the Gathering scripts as well.

[SOLVED]

not sure if I did something wrong, but here’s what is going on in my script instead:

  • Subscriptions are only done to the Animation Event Relay, which get called as Events on the animation itself. I don’t think there’s a connection in ‘PlayerPickupState.cs’ to the ‘OnPickup’ event in ‘InputReader.cs’

I think this was done in ‘Enter()’, not sure… (there’s a check for null targets in there). If we are speaking of the finder gameObjects for each system, these are set up too

not sure of that either…

Yup, we did that

yup. Did that too, even for player gathering

If it helps, this is my current ‘PlayerPickupState.cs’:

using RPG.Control;
using RPG.Inventories;
using UnityEngine;

namespace RPG.States.Player 
{

    public class PlayerPickupState : PlayerBaseState 
    {

        private static readonly int PickupHash = Animator.StringToHash("Pickup");

        private PickupTarget target;
        private Vector3 position;

        public PlayerPickupState(PlayerStateMachine stateMachine, PickupTarget 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(PickupHash, AnimatorDampTime);
            stateMachine.AnimationEventRelay.PickupItemEvent += AnimationEventRelay_HandlePickup;
        }

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

        public override void Exit() 
        {
            stateMachine.AnimationEventRelay.PickupItemEvent -= AnimationEventRelay_HandlePickup;
        }

        void AnimationEventRelay_HandlePickup() 
        {
            target.PickupItem();
        }

    }

}

I think the major difference between the two, as I re-read the gathering state script, is that for pickup, you end the animation because you don’t find anything else within range, but for gathering you don’t… Not sure, still trying to follow up with what’s going on

LOL after quite a long search, I notice this:

and then I notice I never coded that… No wonder none of this was working lel (well it still isn’t working, but at least now I know something went missing)

Edit 2: Fixed it, but still not working… (I’m guessing I’m accidentally making things worse at this point. I’m literally going clueless). Here’s what I did:

In ‘ResourceFinder.cs’, I integrated this code:

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

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

In ‘ResourceGathering.cs’, I included the ‘OnGatheredItem’ event, along with a public function to call from ‘ResourceGatherer.cs’:

public event System.Action<ResourceGathering> OnGatheredItem;

public void InvokeOnGatheredItem() 
        {
            OnGatheredItem?.Invoke(this);
        }

And finally, in ‘ResourceGatherer.Gather()’, I called the event function:

                target.InvokeOnGatheredItem();

Still not working though… (Sometimes, like now, I really wish we can just go back to ‘GetKeyDown(KeyCode.W))’ kinda code… events can get mind-boggling at times)

AND AFTER EVERYTHING I’VE BEEN THROUGH, I COME TO THE REALIZATION THAT BECAUSE I WAS DEVELOPING A BRAND NEW SYSTEM, THIS WAS THE MAJOR PROBLEM (and annoyingly enough, none of what I coded in this comment actually mattered):

I went back to investigate how in the world ‘GetNormalizedTime()’ works, as it was linked in ‘PlayerGatheringState.Enter()’, and then I noticed the input was a tag, so I had to re-tag my animations to whatever ‘ResourceGatherer.GetGatherAnimation()’ had… What a sleep-stressor and headache :sweat_smile:

Frankly speaking though, this was essential. It gave me a significantly larger understanding of how some of this stuff actually works

OK I edited this comment because I solved the ‘Keep playing the animation as long as the assigned Input reader button is clicked’ problem (lowkey cheated from the attacking state, xD)

BUT… I have one request, and a brand new problem:

[PROBLEM]

the player doesn’t recognize respawned sources yet, for some reason… Once the resource is dead, it’s like permanently dead, and there’s literally no way for me to harvest it again when it respawns. I tried messing around a bit with ‘IsValid()’ and other solutions, but to no avail

[REQUEST]

How do we save the resources based on their spawn time, in ‘ResourceGathering.cs’, using our JSON Saveable Entity System? If, for example, I introduce a tree stump to replace a tree after its death, and the tree respawn timer isn’t up, I want the stomp to take its position until it respawns…

The idea is that for some resource sources, the respawn time can be incredibly long (for the rare items at least), and it makes absolutely no sense to make the respawn timer count based on the game timer instead of the global computer timer for this case

I’ll be honest… I’ve gone over this, and I no longer know what you’re actually doing… The comment marked [Solved] looks like it’s not… I can’t tell for sure.

I’m extra hindered because without contaminating the tutorial project with all your added systems, I can’t put these scripts in for testing, and I’m not going there.

I’m also not just going to write the correct script for you, because… you’re not gonna get it if I do…

When you respawn, all the conditions should be reset, so what’s not getting reset that’s causing IsValid() to no longer be Valid?

For this, you need to save a DateTime with the Date and Time that the respawn will occur, and then when you restore, compare that to DateTime.Now
The catch here is that DateTime is not [Serializable], so you’ll have to do a little converting to save it with the saving system…
For that, on the DateTime page, check out ToBinary() and FromBinary(). ToBinary() returns a long (64 bit integer) which can be returned as a JToken. You can deserialize that in Restore with state.ToObject() and use FromBinary() with the result to get that DateTime.

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

Privacy & Terms