Resource Gathering for Third person

OK that worked (to a great extent), but I noticed a new problem… and a bug (which is just natural evolution from what we did):

[PROBLEM]
if you exit the game and don’t finish mining the rock or cutting the tree and return, then to the game you didn’t even begin harvesting. I think we didn’t add that to the saving system.

So let’s assume we mined 5 out of the 10 resources available, and quit the game and returned, to the game you didn’t mine anything last time, so now you got 10 resources available (although you should only be having 5 left)

[BUG]
This one is hard to explain, but I’ll try my best:

Occasionally (THAT"S THE BIG PROBLEM, IT’S OCCASIONAL), the rock that respawns when we return to a load scene will not be detected by the targeter that Brian created for our third person transition, so the error we previously had that was solved by creating an event (check the chat above) in ‘ResourceGathering.cs’ and subscribing to it is suddenly back… I don’t know how or why, but it’s there again (it’s the exact same problem me and Brian were trying to solve prior to the Saving System)

here’s the first NRE (when I hit the mapped key):

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:56)
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 the second 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)

Yeah, if you don’t save it, it won’t get restored.

So, why do you think this is happening?

I gave it a go, but I’m sure that’s not the best way around it. Can you please have a look? It was all very simple modifications:

public JToken CaptureAsJToken()
    {
        var data = new ResourceData(destroyedTime, IsCollected(), resourceToSpawn.GetQuantityLeft());
        return JToken.FromObject(data);
    }

    public void RestoreFromJToken(JToken state)
    {
        var data = state.ToObject<ResourceData>();
        destroyedTime = data.DestroyedTime;

        var shouldBeCollected = data.ShouldBeCollected;
        var quantityLeft = data.QuantityLeft;

        if (shouldBeCollected && !IsCollected())
        {
            DestroyResource(false);
        }
        if (!shouldBeCollected && IsCollected())
        {
            SpawnResource();
            resourceToSpawn.quantityLeft = quantityLeft;
        }
    }
}

[Serializable]
public struct ResourceData
{
    public double DestroyedTime;
    public bool ShouldBeCollected;
    public int QuantityLeft;

    public ResourceData(double destroyedTime, bool shouldBeCollected, int quantityLeft)
    {
        DestroyedTime = destroyedTime;
        ShouldBeCollected = shouldBeCollected;
        QuantityLeft = quantityLeft;
    }
}

}

as far as I understood, it’s mainly happening if I respawn the player back into the game scene at the same moment a rock (or tree, or any other resource gathering system…) gets respawned…? Not too sure to be honest

It’s not really the spawner’s job to store the resource’s quantity. But I guess it will do

Yes, but I asked why you think it’s happening, not when

well… with my current attempt, it did not work for some reason :sweat_smile: (and if it’s not the job of the spawner, then I guess it’s best we clean it, for good coding purposes because one day I’ll return to read this behemoth of a program… :stuck_out_tongue_winking_eye: ). To give that a second go, I tried doing it in ‘ResourceGathering.cs’. Is this correct?:

        public JToken CaptureAsJToken()
        {
            return JToken.FromObject(GetQuantityLeft());
        }

        public void RestoreFromJToken(JToken state)
        {
            var data = state.ToObject<int>();
            quantityLeft = data;
        }

well… from recent experience, it’s because we did not subscribe the rock to the ‘OnResourceDestroyed’ event in ‘ResourceGathering.cs’, which removes it from the targeter, but we already did that in ‘ResourceRespawner.cs’, right? (I mean it’s the last line of ‘ResourceRespawner.SpawnResource()’, and the first line of ‘ResourceRespawner.DestroyResource()’)

Is it because we have two destroy functions, one being in ‘ResourceRespawner.cs’, and one in ‘ResourceGathering.cs’, and ‘ResourceGatherer.cs’ uses the one in ‘ResourceGathering.cs’?

Does it work?

Close, but no. This is not how events work. ResourceGathering is the sender. It does not subscribe to it. The spawner is a subscriber / listener / handler.
So, this problem is on me. I did not consider this. I need to check the spawner and see how we can fix this. But it’s 3am and I need to work in the morning

nope :sweat_smile:

no worries man, have a good night rest. Will be waiting to hear from you again tomorrow (if, miraculously, I manage to solve it, I’ll let you know :slight_smile:)

so as not to baffle the confused guy here (me), who is trying to get better (I don’t know how or when, but it usually just hits me like a flood one day, where everything makes sense… I’m waiting for that day), let’s just stick to calling it as a “subscriber” :stuck_out_tongue_winking_eye:

So, the problem I created was that the saving system will destroy the resource if it shouldn’t be there, but the targeter may already have a reference to it. When the spawner destroys the resource, it doesn’t fire an event so the targeter never knows that the resource was destroyed.
Here’s a fix. I have rearranged the code a little so that the spawner tells the resource to destroy itself. Now the event will fire as usual and the targeter will know what to do

First thing we have to do is update ResourceGathering.DestroyResource() to remove the parent from the resource when the resource is destroyed (it causes a bug that we may as well fix now). I wrote about this like 2 years ago or something (Edit: Found it. It’s here). The portal loads and then saves in the same frame. The load may destroy the resource, but then it’s still around when the save happens and then it’s all messed up. Here’s the update

public void DestroyResource()
{
    OnResourceDestroyed?.Invoke(this);
    transform.SetParent(null); // unset the parent
    Destroy(gameObject);
    isDestroyed = true;
}

And here’s the updated ResourceSpawner. It does not include the saving bits for the quantity. That’s an exercise for you.

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

namespace RPG.ResourceManager
{

    public class ResourceRespawner : MonoBehaviour, IJsonSaveable
    {
        [SerializeField] ResourceGathering resourceToSpawn;
        [SerializeField] int hideTime;

        private double destroyedTime;
        private TimeKeeper timeKeeper;

        private void Awake()
        {
            timeKeeper = TimeKeeper.GetTimeKeeper();
            SpawnResource();
        }

        public ResourceGathering GetResource()
        {
            return GetComponentInChildren<ResourceGathering>();
        }

        public bool IsCollected()
        {
            return GetResource() == null;
        }

        private void SpawnResource()
        {
            var resourceObject = Instantiate(resourceToSpawn, transform.position, Quaternion.identity);
            resourceObject.transform.SetParent(transform);
            resourceObject.OnResourceDestroyed += OnResourceDestroyed;
        }

        private IEnumerator WaitAndRespawn()
        {
            var elapsedTime = timeKeeper.GetGlobalTime() - destroyedTime;
            yield return new WaitForSeconds(hideTime - elapsedTime);
            SpawnResource();
        }

        private void OnResourceDestroyed(ResourceGathering resourceNode)
        {
            resourceNode.OnResourceDestroyed -= OnResourceDestroyed;
            destroyedTime = timeKeeper.GetGlobalTime();
            StartCoroutine(WaitAndRespawn());
        }

        public JToken CaptureAsJToken()
        {
            var data = new ResourceData(destroyedTime, IsCollected());
            return JToken.FromObject(data);
        }

        public void RestoreFromJToken(JToken state)
        {
            var data = state.ToObject<ResourceData>();
            destroyedTime = data.DestroyedTime;

            var shouldBeCollected = data.ShouldBeCollected;
            if (shouldBeCollected && !IsCollected())
            {
                var resourceObject = GetResource();
                resourceObject.OnResourceDestroyed -= OnResourceDestroyed;
                resourceObject.DestroyResource();
                StartCoroutine(WaitAndRespawn());
            }
            if (!shouldBeCollected && IsCollected())
            {
                SpawnResource();
            }
        }
    }

    [Serializable]
    public struct ResourceData
    {
        public double DestroyedTime;
        public bool ShouldBeCollected;

        public ResourceData(double destroyedTime, bool shouldBeCollected)
        {
            DestroyedTime = destroyedTime;
            ShouldBeCollected = shouldBeCollected;
        }
    }
}

OK so… I gave this a second try, but for some reason it’s not coming along well. Here’s what I tried doing (all changes are in ‘ResourceRespawner.cs’):

  1. In the resource data struct, I integrated a new variable called ‘QuantityLeft’, as follows:
[Serializable]
public struct ResourceData
{
    public double DestroyedTime;
    public bool ShouldBeCollected;
    public int QuantityLeft;

    public ResourceData(double destroyedTime, bool shouldBeCollected, int quantityLeft)
    {
        DestroyedTime = destroyedTime;
        ShouldBeCollected = shouldBeCollected;
        QuantityLeft = quantityLeft;
    }
}

  1. When capturing the JToken, I integrated the ‘GetQuantityLeft()’ function I created, so this is what the new data line looks like:
        var data = new ResourceData(destroyedTime, IsCollected(), GetQuantityLeft());
  1. When restoring the JToken, here’s what I added:
// similar to getting the destroyedTime and 'shouldBeCollected':
        var quantityLeft = data.QuantityLeft;

// in the second if statement of the restore function:
            GetResource().quantityLeft = quantityLeft;

So the final saving system looks like this:

// new function:
    public int GetQuantityLeft() 
    {
        return GetResource().GetQuantityLeft();
    }

// saving stuff:
public JToken CaptureAsJToken()
    {
        var data = new ResourceData(destroyedTime, IsCollected(), GetQuantityLeft());
        return JToken.FromObject(data);
    }

    public void RestoreFromJToken(JToken state)
    {
        var data = state.ToObject<ResourceData>();
        destroyedTime = data.DestroyedTime;

        var shouldBeCollected = data.ShouldBeCollected;
        var quantityLeft = data.QuantityLeft;

        if (shouldBeCollected && !IsCollected())
        {
            var resourceObject = GetResource();
            resourceObject.OnResourceDestroyed -= OnResourceDestroyed;
            resourceObject.DestroyResource();
            StartCoroutine(WaitAndRespawn());
        }
        if (!shouldBeCollected && IsCollected())
        {
            SpawnResource();
            GetResource().quantityLeft = quantityLeft;
        }
    }
}

[Serializable]
public struct ResourceData
{
    public double DestroyedTime;
    public bool ShouldBeCollected;
    public int QuantityLeft;

    public ResourceData(double destroyedTime, bool shouldBeCollected, int quantityLeft)
    {
        DestroyedTime = destroyedTime;
        ShouldBeCollected = shouldBeCollected;
        QuantityLeft = quantityLeft;
    }
}

SOO… why is this still not working?

This actually highlights an issue I didn’t consider and we should address.

There are 4 states the spawner could be in, but only 2 of them are handled. The same is true for the pickup spawner, but it’s not an issue for the pickup spawner because the unhandled states are implicitly handled.

  • Handled
    • The resource is spawned but should not be spawned
    • The resource is not spawned but should be
  • Not handled
    • The resource is spawned and should be spawned
    • The resource is not spawned and should not be spawned

We have to handle the two unhandled cases.

  • The resource is spawned and should be spawned.
    We have to handle this case because a new save is loaded and the quantity should be updated to whatever is saved.
  • The resource is not spawned and should not be spawned.
    We have to stop the timer and recreate it with the saved start time

You may want to update RestoreFromJToken with something like this

public void RestoreFromJToken(JToken state)
{
    var data = state.ToObject<ResourceData>();
    destroyedTime = data.DestroyedTime;

    var shouldBeCollected = data.ShouldBeCollected;
    var quantityLeft = data.QuantityLeft;

    if (shouldBeCollected && !IsCollected())
    {
        // Should be destroyed but isn't - destroy it
        var resourceObject = GetResource();
        resourceObject.OnResourceDestroyed -= OnResourceDestroyed;
        resourceObject.DestroyResource();
        StartCoroutine(WaitAndRespawn());
    }
    else if (!shouldBeCollected && IsCollected())
    {
        // Shouldn't be destroyed but is - spawn it
        SpawnResource();
        GetResource().quantityLeft = quantityLeft;
    }
    else if (shouldBeCollected && IsCollected())
    {
        // Should be destroyed and is - reset timer
        StopAllCoroutines();
        StartCoroutine(WaitAndRespawn());
    }
    else
    {
        // Shouldn't be destroyed and isn't - reset quantity
        var resourceObject = GetResource();
        GetResource().quantityLeft = quantityLeft;
    }
}

I don’t like all the if’s here, but let’s get it working

OK so that worked perfectly between save files, where I tried loading another file and returning to the previous one to confuse the project, but it identified them perfectly. However, there is one major problem:

NullReferenceException: Object reference not set to an instance of an object
RPG.ResourceManager.ResourceRespawner.GetQuantityLeft () (at Assets/Project Backup/Scripts/ResourceManager/Old Scripts/ResourceRespawner.cs:36)
RPG.ResourceManager.ResourceRespawner.CaptureAsJToken () (at Assets/Project Backup/Scripts/ResourceManager/Old Scripts/ResourceRespawner.cs:62)
GameDevTV.Saving.JSONSaveableEntity.CaptureAsJToken () (at Assets/Project Backup/Scripts/Saving/JSON Saving System/JSONSaveableEntity.cs:29)
GameDevTV.Saving.JSONSavingSystem.CaptureAsToken (Newtonsoft.Json.Linq.JObject state) (at Assets/Project Backup/Scripts/Saving/JSON Saving System/JSONSavingSystem.cs:134)
GameDevTV.Saving.JSONSavingSystem.Save (System.String saveFile) (at Assets/Project Backup/Scripts/Saving/JSON Saving System/JSONSavingSystem.cs:44)
RPG.SceneManagement.SavingWrapper.Save () (at Assets/Project Backup/Scripts/SceneManagement/SavingWrapper.cs:190)
RPG.UI.PauseMenuUI.SaveAndQuit () (at Assets/Project Backup/Scripts/UI/PauseMenuUI.cs:55)
UnityEngine.Events.InvokableCall.Invoke () (at <ba783288ca164d3099898a8819fcec1c>:0)
UnityEngine.Events.UnityEvent.Invoke () (at <ba783288ca164d3099898a8819fcec1c>:0)
UnityEngine.UI.Button.Press () (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/UI/Core/Button.cs:70)
UnityEngine.UI.Button.OnPointerClick (UnityEngine.EventSystems.PointerEventData eventData) (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/UI/Core/Button.cs:114)
UnityEngine.EventSystems.ExecuteEvents.Execute (UnityEngine.EventSystems.IPointerClickHandler handler, UnityEngine.EventSystems.BaseEventData eventData) (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/ExecuteEvents.cs:57)
UnityEngine.EventSystems.ExecuteEvents.Execute[T] (UnityEngine.GameObject target, UnityEngine.EventSystems.BaseEventData eventData, UnityEngine.EventSystems.ExecuteEvents+EventFunction`1[T1] functor) (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/ExecuteEvents.cs:272)
UnityEngine.EventSystems.EventSystem:Update() (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/EventSystem.cs:514)
  1. This NRE is a major issue, because it will show up when the resource source we just harvested is dead, and we will never be able to save and quit the game until that resource comes back to life. After a bit of investigation, I tracked it down to this function:
    public int GetQuantityLeft() 
    {
        return GetResource().GetQuantityLeft();
    }

and if you need to know what ‘GetResource()’ contains, here you go:

    public ResourceGathering GetResource()
    {
        return GetComponentInChildren<ResourceGathering>();
    }

and ‘GetQuantityLeft()’ from ‘ResourceGathering.cs’:

        public int GetQuantityLeft() {
            return quantityLeft;
        }

(P.S: I tried to change them into variables instead of called functions, but that messed things up…)

  1. Through minor testing (will still extensively test that), I noticed something new… the respawn timer does not seem to work when the game is paused. So if you press pause (not the Unity Engine pause, but the ‘Escape’ key pause that we implemented in-game), the respawn timer will pause as well (I’ll test it with shorter times for consistency). Not sure if that happens or not, just something I noticed on the fly… Can you keep me updated about how this works?

(For the second point, it’s true. For the rock (or any resource) to respawn, we must be out of the pause menu for the timer to count… Is it possible to make the timer count independent of the pause menu status?)

Yeah, if the resource is dead GetResource() will return null. You would have to check it. You can return 0 if it is null because it wouldn’t matter if the resource is dead (and that’s what the value of a dead resource would be anyway)

The game is paused, so it won’t work. The global timer doesn’t care about pauses, but the coroutine does. This means the actual spawn time will be hideTime plus the total pause duration. If the hideTime is 5 minutes and you pause for 30 seconds and then for another 1 minute and 30 seconds, the resource will only spawn after 7 minutes because the timer will wait for the correct duration, but also not run for a total of 2 minutes. You could go back to the 30 second (or 1 second for short respawns) wait loop. Then the timer won’t accumulate as much of the pause duration because after the unpause it will wait - at most - 30 seconds before checking if the resource should spawn

at first it made a mistake where it confused two save files’ quantityLeft values, but then it got things right through 3 different files, so all is good for that for now

Are we talking about this?

Yeah

why don’t we just use the ‘WaitForSecondsRealtime’ function instead? I literally just noticed that thing exists by accident…

Because I’ve never used it before and it doesn’t spring to mind in cases like this. It should work

oh it worked perfectly lel… it respawned the rock as well independent of whether the game was paused or not. This system is, to me at least, perfect now!

Sorry for the hassle my friend :slight_smile: - I’ll go take a short break before determining what my next problem will be :stuck_out_tongue_winking_eye:

keep this open, just in case… will swap it to talk

This topic was automatically closed 20 days after the last reply. New replies are no longer allowed.

Privacy & Terms