Merging the Third Person Controller Course with the RPG Course

Ideally, stopping on an issue and getting it sorted before moving on to the next issue… When you plow through and just let the issues pile up, that’s when things can get unmanageble. Most issues can be solved comparing the Repo to your own code (or in the case of the blend trees, reviewing the lesson again… Messaging is not ideal, I’m looking in to creating a proper topic tag set for the tutorial, but it’s not GamedevTV official, so that may not work.

OK so… I solved 2 issues so far:

  • To get the enemy to chase you, on top of the code, ensure he has a ‘ForceReceiver.cs’ script attached to him (I forgot about that…), but he definitely needs a limit before he can start attacking the player… having an enemy literally try to get into your player is weird, even beyond his death :sweat_smile:
  • To get the animations to work, for the targeting state (which was where my issues were), ensure that:

a. naming is right for all your blend trees
b. the internal blend trees are 2D, with the right parameter orders, and the big map (in the 2D Cartesian option) corresponds properly to the player input

but the issue of my player occasionally being unable to enter targeting state, when he loads up a game save, for some reason, in the game build, still persists (it’s a rare one, but it happens from time to time)

you may also want to let the player know that when they’re setting up their patrol paths, to use the ones in the scene, not the ones as prefabs, otherwise it won’t work :sweat_smile:

This issue is covered in the RPG Core Combat course which is an absolute (you can’t build this project from my directions without it) requirement before going through the tutorial.

ehh just leaving it there, because I made that mistake… and then I remembered the nightmare we had when I was trying to randomize the waypoints and dwell time (I’ll figure out how to inject that code when this transition is done. That, and the RespawnManager.cs (that script relied on ‘AIController.cs’, and that is no longer available, so…), because enemies now are in an unplayable state… their death is terrible :stuck_out_tongue_winking_eye: )

I’ll go back now to watching how they build cars I can only dream of… :slight_smile:

umm, Brian… you may want to have a look at this: Messy Chase State - Google Drive

the chase state is a bit messy… (watch till the end)

The issue where he becomes insanely clingy to the player frequently happens pre-death of the enemy as well :slight_smile:

and the problem of the targeting state being occasionally non-accessible by the player, when he returns to an old save system, for some reason, is becoming a significant one… (it’s all outside the engine, so it’s really hard for me to identify where the issue is coming from, I’ll investigate it ASAP within Unity itself). At this point in time, I have a feeling like it’s because the Enemy Death and Respawn State has not been addressed yet, so I will wait before re-investigating :slight_smile:

I know I said I’ll be trying to work a resource gathering saving system first, but since temptation took over and I implemented the death state first, I figured this is important to bring up, before forgetting about it (two problems, one request):

[PROBLEMS]

  1. When the player dies, he literally respawns to the position he died at (so you die, and then respawn only to die by the same boss again? LOL). How do I get him to respawn to the players’ original position/closest respawn checkpoint instead? (If you die then save and quit the game immediately, this is not an issue, but it’s an issue if you don’t save and quit the game immediately after your death… You can see how this can be a nuisance)

Problem 2: When you’re out of combat, if you save and quit the game, the player spawns at the game starting point, not where he last was in the game before saving and quitting…

[REQUEST]

  1. can this tutorial have a remake for the third person? It’s the Respawn tutorial that I requested almost a year ago :slight_smile: - it was one of the best parts for me of the entire project tbh, it would be terrible if I just let this go

This should have been covered in the Shops and Abilities course. I’ll have to research it, it’s happening automatically with the course project.

Very low on priority list, atm. I’m not sure, however, that it actually needs adapting. The biggest thing we’re doing with this conversion is swapping out the PlayerController/AIController for formal State Machines. If I recall, the Respawn Tutorial simply detects the death, delays a bit, then respawns the characteer. Shouldn’t be affected by the Third Person conversion in the slightest.

well… It’s not happening automatically anymore :sweat_smile:

exactly what’s causing me the trouble… What other code, apart from the State Machine, do we need to modify to get this into the new system? I’m sure just swapping out ‘AIController.cs’ data type to ‘EnemyStateMachine.cs’ isn’t all of it

As of my last update, this is what the ‘RespawnManager.cs’ script we created looked like:

using System.Collections.Generic;
using GameDevTV.Saving;
using RPG.Attributes;
using RPG.Control;
using UnityEngine;
using RPG.Combat;
using Newtonsoft.Json.Linq;
using RPG.Dialogue;
using RPG.Core;
using RPG.States.Enemies;

namespace RPG.Respawnables
{

    // Since SaveableEntity relies on the ISaveable information of each character to
    // bundle it into our Save file, there are cases when our enemies' state is non-existant
    // hence we need to create a brand new RespawnManager class, as shown below:

    public class RespawnManager : SaveableEntity, IJsonSaveable //, ISaveable in Saving below, we will have to replace the logic, hence we inherit from 'SaveableEntity.cs'
    
    {

        // This class will handle the following:

        // spawning our enemy to the scene
        // listen to death notifications (i.e: Enemy is dead, proceed to keeping him dead for the following steps)
        // hide the body after 'hideTime'
        // deleting the enemy after 'hideTime' is complete
        // Respawn the enemy after 'respawnTime'
    
        [SerializeField] AIController spawnableEnemy;   // the enemy to spawn/respawn
        [HideInInspector] private AIController lastSpawnableEnemy;  // this AI Controller ensures its an enemy we are respawning
        [SerializeField] private float hideTime = 60;   // time before hiding our dead character
        [SerializeField] private float respawnTime = 90;    // time before respawning our hidden dead character, as an alive character
        [SerializeField] PatrolPath patrolPath; // the path our character will follow, from his spawn point
        [SerializeField] AggroGroup aggroGroup; // aggrevated group of guards, based on wrong dialogue player has said
        [SerializeField] bool hasBeenRestored;  // checks if the enemy has been restored before fading in from the main menu, or a load scene, or not

        // TEST Variables: Delete if failed
        /* [SerializeField] AggroGroup aggroGroup; // the group of enemies we want attacking our player, if things go bad
        [SerializeField] DialogueAggro dialogueAggro; */

        private AIController spawnedEnemy;

        // TEST CODE:
        private bool isConversant;
        private PlayerConversant playerConversant;

        /* void Awake() {

            playerConversant = playerConversant.GetComponent<PlayerConversant>();
            isConversant = TryGetComponent(out AIConversant conversant);

        } */

        // --------------------------- NOTE: RestoreState() occurs BEFORE Start(), hence we need to ensure everything works accordingly --------------

        private void Start()
        {
            // Check if the Enemy has been restored first or not, prior to Respawning him (ensuring RestoreState(), which occurs first, works properly)
            if (!hasBeenRestored) Respawn();

        }

        private void Respawn()
        {

            if (spawnedEnemy)
            {

                // Dude is not dead no longer, so delete his previous 'onDeath' record after he's respawned
                spawnedEnemy.GetComponent<Health>().onDie.RemoveListener(OnDeath);

            }

            foreach (Transform child in transform)
            {
                // Start the Respawn by deleting any existing gameObjects
                Destroy(child.gameObject);
            }

            // Respawn the enemy, and parent the enemy to our respawnManagers' transform
            spawnedEnemy = Instantiate(spawnableEnemy, transform);

            // Get the spawned/respawned enemies' health, and listen for death notifications
            spawnedEnemy.GetComponent<Health>().onDie.AddListener(OnDeath);

            if (patrolPath != null)
            
            {

                Debug.Log($"Assigning Patrol Path {patrolPath} to {spawnedEnemy.name}");
                spawnedEnemy.AssignPatrolPath(patrolPath);
                // Update();
            }

            else
            
            {
                Debug.Log($"No Patrol Path to assign");
            }

            // --------------------------- Extra Functionality: Setting up Aggro Group + Adding Fighters ---------------

            // First add the fighters, and then the Dialogue Guard (if there's one), because doing the opposite
            // means that we will not be adding fighters if there' no dialogue guard (which is not something we
            // want because we need to prioritize fighters over dialogue)

            if (aggroGroup != null)
            {
                aggroGroup.AddFighterToGroup(spawnedEnemy.GetComponent<Fighter>());
                if (spawnedEnemy.TryGetComponent(out DialogueAggro dialogueAggro)) //aggrogroup is at this point valid
                {
                    dialogueAggro.SetAggroGroup(aggroGroup);
                }
            }

            // ---------------------------------------------------------------------------------------------------------

        }

        void HideCharacter()
        {
            // Hide the dead character
            foreach (Renderer renderer in spawnedEnemy.GetComponentsInChildren<Renderer>())
            {
                renderer.enabled = false;
            }
        }

        void OnDeath() {

            // hide the character after 'hideTime', and then respawn him after 'respawnTime'
            Invoke(nameof(HideCharacter), hideTime);
            Invoke(nameof(Respawn), respawnTime);
            
            // Test (Delete if failed): disable the spawnableEnemy, until he comes back to life
            // spawnableEnemy.enabled = false;

            // TEST: Delete if failed
            if (aggroGroup != null) {

                aggroGroup.RemoveFighterFromGroup(spawnedEnemy.GetComponent<Fighter>());

            }

            // if he quit the game and returned, deactivate the dead dude until he can respawn

        }

        public void WaitingPeriod() {

            // Get a little suspicious before patrolling again
            
            spawnedEnemy.SuspicionBehaviour();

        }

        public JToken CaptureAsJToken()
        {

            JObject state = new JObject();
            IDictionary<string, JToken> stateDict = state;

            foreach (IJsonSaveable JSONSaveable in spawnedEnemy.GetComponents<IJsonSaveable>())
            {
                JToken token = JSONSaveable.CaptureAsJToken();
                string component = JSONSaveable.GetType().ToString();
                Debug.Log($"{name} Capture {component} = {token.ToString()}");
                stateDict[JSONSaveable.GetType().ToString()] = token;
            }

            return state;

        }

        public void RestoreFromJToken(JToken s)
        {

            JObject state = s.ToObject<JObject>();
            IDictionary<string, JToken> stateDict = state;

            foreach (IJsonSaveable jsonSaveable in spawnedEnemy.GetComponents<IJsonSaveable>())
            {

                string component = jsonSaveable.GetType().ToString();
                if (stateDict.ContainsKey(component))
                {

                    Debug.Log($"{name} Restore {component} => {stateDict[component].ToString()}");
                    jsonSaveable.RestoreFromJToken(stateDict[component]);

                }

            }

        }

        private void OnValidate()
        {

            // This function checks if we changed the Spawnable Character
            // (ensuring any character we attach to the spawnableEnemy is automatically added
            // to the RespawnManager when editing the game, 
            // hence we dont accidentally spawn the wrong dude at the right spot)

            if (spawnableEnemy != lastSpawnableEnemy)
            {
                lastSpawnableEnemy = spawnableEnemy;
                
                foreach (Transform child in transform) {

                    Destroy(child.gameObject);

                }

                Instantiate(spawnableEnemy, transform);

            }
        }
    }
}



// THE ARCHER AND ENEMY 2 ARE BOTH CHILDREN OF THE 'CHARACTER' PREFAB. DISCONNECT THEM FROM THAT, OTHERWISE THEY'LL KEEP INSTANTIATING
// FROM UNDER THE QUEST GIVER, WHO IS ALSO A PREFAB FROM THE CHARACTER PREFAB!!!

You know we’re not using the AIController… so that might clue you in that you need to replace references to AIController with EnemyStateMachine()

I don’t even see where lastSpawnableEnemy is used, can’t find it in the rest of the script…
Change AIController spawnableEnemy; to EnemyStateMachine spawnableEnemy;
Change AIController spawnedEnemy to EnemyStateMachine spawnedEnemy;

spawnedEnemy.PatrolPath = patrolPath;

I don’t see where this is getting called in the code at all. Let the StateMachine worry about delays. If you really want a delay before doing anything, then the StateMachine should have some sort of Delay/Wait State that sets the animator to Idle and then counts down a timer before calling EnemyIdleState. In a later lesson We’ll be discussing using selectable states – so some sort of enum for each type of state (Idle, Chasing, Suspicious, Attacking) that determines the type of behaviour you want to employ, and requesting the StateMachine itself provide an acceptable state for each desired condition. Again, later lesson, as in weeks from now… in fact, there will be a haitus on the Third Person for a few weeks as I need to rebuild and modernize the Saving System

it’s used in ‘OnValidate()’

this one was pointless, I agree with you on this one… my code needs a bit of cleaning, but it’s not that easy as I’m still learning what goes where

no worries, I’m more than happy to be patient :slight_smile: - in the meanwhile I’ll probably explore other systems, starting with the saving system for the resource respawner (I took the day off yesterday to get some rest)

for now I’ll just change the AIControllers to State Machines and call it a day

The OnValidate code should probably die anyways, TBH. I completely stripped it from my own Respawn Manager system because Unity was complaining getting unstable in later versions of Unity if you Destroy GameObjects… You can add the appropriate character manually to each prefab.

lol wait till you figure out the optimization issues the aggroGroup and respawners caused by the respawn managers are throwing at me… (long story for another day)

Long Story short, Unity does not delete respawned enemies after the game simulation ends, so they constantly rack up and give me serious performance issues. We tried tackling this before and failed

anyway, this did not work to make it respawn, and I’m a little too tired for that now. We’ll take care of it another day, as my resource gathering system still needs a respawn time saver first :slight_smile:

At some point you are going to need to figure these things out… :slight_smile: That or I haven’t been a very good teacher.

Honestly, you’re one of THE BEST teachers I had so far, but you have one of the slowest learning students, known as me… :stuck_out_tongue_closed_eyes: (I know this might sound overfetched, but I struggle to understand concepts quickly. That’s why sometimes it takes me days or long hours to get what we’re talking about, and I apologize that I didn’t tell you this earlier)

Bear with me, please :sweat_smile:

Hi there @Brian_Trotter :slight_smile:

Just finished the current part of the merge tutorial, having a blast with it so far.

I’ve encountered a problem trying to bind all the interact action (dialogue, shop, pickup) to the same key (E).

I tried both having them as separate actions (in the control map, that is Shop / Dialogue /Pickup be separate actions all bound to “E”), and also tried having them as a single interaction (called interact, on the player map) and simply calling all the events. Had the same errors.

These are the errors I’m getting:

1:
MissingReferenceException: The object of type 'PickupTarget' 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/Scripts/Core/RangeFinder.cs:57)
RPG.States.Player.PlayerFreeLookState.InputReader_HandlePickupEvent () (at Assets/Scripts/States/Player/PlayerFreeLookState.cs:129)
RPG.States.Player.PlayerFreeLookState.HandleAttackButtonPressed () (at Assets/Scripts/States/Player/PlayerFreeLookState.cs:76)
RPG.States.Player.PlayerFreeLookState.Tick (System.Single deltaTime) (at Assets/Scripts/States/Player/PlayerFreeLookState.cs:35)
RPG.States.StateMachine.Update () (at Assets/Scripts/States/StateMachine.cs:18)
2:
MissingReferenceException: The object of type 'PickupTarget' 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/Scripts/Core/RangeFinder.cs:57)
RPG.States.Player.PlayerFreeLookState.InputReader_HandlePickupEvent () (at Assets/Scripts/States/Player/PlayerFreeLookState.cs:129)
RPG.InputReading.InputReader.OnInteract (UnityEngine.InputSystem.InputAction+CallbackContext context) (at Assets/Scripts/InputReading/InputReader.cs:141)
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)
3:
MissingReferenceException while executing 'started' callbacks of 'Player/Interact[/Keyboard/e]'
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)
4:
MissingReferenceException while executing 'performed' callbacks of 'Player/Interact[/Keyboard/e]'
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)
5:
MissingReferenceException while executing 'canceled' callbacks of 'Player/Interact[/Keyboard/e]'
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)

It’s not happening consistenly, only some of the times.

It appears to be happening when I loot too soon after a fight, or when I’m fighting next to loot I’m not entirely sure.
I tried following the guide as closely as possible, And I know there is a part where you say to be mindful of calling events in the particular order stated which I think I am doing correctly.

I have an event on the Pickup animation (which isn’t being played when the error happens, but it all works well when it does play).

Here’s my scripts :

PlayerPickupState .cs
using RPG.Inventories;
using UnityEngine;

namespace RPG.States.Player
{
	public class PlayerPickupState : PlayerBaseState
	{
		private static readonly int PickupHash = Animator.StringToHash("Pickup");

		public PlayerPickupState(PlayerStateMachine stateMachine, PickupTarget target) : base(stateMachine)
		{
			this.target = target;
		}

		private PickupTarget target;
		private Vector3 position;

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

using UnityEngine;

namespace RPG.States.Player
{
public class PlayerFreeLookState : PlayerBaseState
{
public PlayerFreeLookState(PlayerStateMachine stateMachine) : base(stateMachine)
{
}

	private static readonly int FreeLookBlendTreeHash = Animator.StringToHash("FreeLookBlendTree");
	private static readonly int FreeLookSpeedHash = Animator.StringToHash("FreeLookSpeed");

	public override void Enter()
	{
		stateMachine.InputReader.JumpEvent += InputReader_HandleJumpEvent;
		stateMachine.InputReader.TargetEvent += InputReader_HandleTargetEvent;
		stateMachine.InputReader.PickupEvent += InputReader_HandlePickupEvent;
		stateMachine.InputReader.DialogueEvent += InputReader_HandleDialogueEvent;
		stateMachine.InputReader.ShopEvent += InputReader_HandleShopEvent;
		stateMachine.Animator.CrossFadeInFixedTime(FreeLookBlendTreeHash, stateMachine.CrossFadeDuration);
	}


	public override void Tick(float deltaTime)
	{
		Vector3 movement = CalculateMovement();
		Move(movement * stateMachine.FreeLookMovementSpeed, deltaTime);

		if (stateMachine.InputReader.IsAttacking)
		{
			HandleAttackButtonPressed();
			return;
		}

		if (stateMachine.InputReader.MovementValue == Vector2.zero)
		{
			stateMachine.Animator.SetFloat(FreeLookSpeedHash, 0, 0.1f, deltaTime);
			if (stateMachine.Animator.GetFloat(FreeLookSpeedHash) < .1f)
			{
				stateMachine.Animator.SetFloat(FreeLookSpeedHash, 0f);
			}

			return;
		}

		stateMachine.Animator.SetFloat(FreeLookSpeedHash, 1, 0.1f, deltaTime);
		FaceMovementDirection(movement, deltaTime);
	}

	public override void Exit()
	{
		stateMachine.InputReader.JumpEvent -= InputReader_HandleJumpEvent;
		stateMachine.InputReader.TargetEvent -= InputReader_HandleTargetEvent;
		stateMachine.InputReader.PickupEvent -= InputReader_HandlePickupEvent;
		stateMachine.InputReader.DialogueEvent -= InputReader_HandleDialogueEvent;
		stateMachine.InputReader.ShopEvent -= InputReader_HandleShopEvent;
	}

	void HandleAttackButtonPressed()
	{
		if (stateMachine.Targeter.HasTargets)
		{
			stateMachine.SwitchState(new PlayerAttackingState(stateMachine, 0));
			return;
		}

		if (stateMachine.PickupFinder.HasTargets)
		{
			InputReader_HandlePickupEvent();
			return;
		}

		if (stateMachine.ConversantFinder.HasTargets)
		{
			InputReader_HandleDialogueEvent();
			if (stateMachine.ConversantFinder.CurrentTarget) return;
		}

		stateMachine.SwitchState(new PlayerAttackingState(stateMachine, 0));
	}

	private void FaceMovementDirection(Vector3 forward, float deltaTime)
	{
		if (forward == Vector3.zero) return;
		Quaternion desiredRotation = Quaternion.LookRotation(forward, Vector3.up);
		stateMachine.transform.rotation =
			Quaternion.Slerp(stateMachine.transform.rotation, desiredRotation,
				stateMachine.FreeLookRotationSpeed * deltaTime);
	}


	private Vector3 CalculateMovement()
	{
		Vector3 forward = stateMachine.MainCameraTransform.forward;
		Vector3 right = stateMachine.MainCameraTransform.right;
		forward.y = 0;
		right.y = 0;
		forward.Normalize();
		right.Normalize();
		Vector3 movement = right * stateMachine.InputReader.MovementValue.x;
		movement += forward * stateMachine.InputReader.MovementValue.y;
		return Vector3.Min(movement, movement.normalized);
	}


	private void InputReader_HandleJumpEvent()
	{
		Debug.Log($"I get up, and nothing gets me down");
		stateMachine.SwitchState(new PlayerFreeLookState(stateMachine));
	}

	private void InputReader_HandleTargetEvent()
	{
		if (stateMachine.Targeter.SelectTarget())
		{
			stateMachine.SwitchState(new PlayerTargetingState(stateMachine));
		}
	}

	private void InputReader_HandlePickupEvent()
	{
		if (stateMachine.PickupFinder.FindNearestTarget())
		{
			stateMachine.SwitchState(new PlayerPickupState(stateMachine, stateMachine.PickupFinder.CurrentTarget));
		}
	}

	private void InputReader_HandleDialogueEvent()
	{
		if (stateMachine.ConversantFinder.FindNearestTarget())
		{
			PlayerConversantState conversantState =
				new PlayerConversantState(stateMachine, stateMachine.ConversantFinder.CurrentTarget);
			stateMachine.SwitchState(new PlayerTurningState(stateMachine,
				stateMachine.ConversantFinder.CurrentTarget.transform.position, conversantState));
		}
	}

	private void InputReader_HandleShopEvent()
	{
		if (stateMachine.ShopFinder.FindNearestTarget())
		{
			PlayerShoppingState nextState =
				new PlayerShoppingState(stateMachine, stateMachine.ShopFinder.CurrentTarget);
			stateMachine.SwitchState(new PlayerTurningState(stateMachine,
				stateMachine.ShopFinder.CurrentTarget.transform.position, nextState));
		}
	}
}

}

InputReader.cs

using System;
using System.Collections;
using System.Collections.Generic;
using RPG.UI;
using UnityEngine;
using UnityEngine.InputSystem;

namespace RPG.InputReading
{
public class InputReader : MonoBehaviour, Controls.IPlayerActions, Controls.IUIActions
{
public bool IsAttacking { get; private set; }
public bool IsBlocking { get; private set; }
public Vector2 MovementValue { get; private set; }

	public event Action JumpEvent;
	public event Action DodgeEvent;
	public event Action TargetEvent;

	public event Action CancelEvent;
	public event Action InventoryEvent;

	public event Action AttackDownEvent;

	public event Action PickupEvent;
	public event Action DialogueEvent;
	public event Action ShopEvent;
	// public event Action InteractEvent;


	private Controls controls;

	private void Start()
	{
		controls = new Controls();
		controls.Player.SetCallbacks(this);
		controls.UI.SetCallbacks(this);

		controls.Player.Enable();
		controls.UI.Enable();

		WindowController.OnAnyWindowOpened += DisableControls;
		WindowController.OnAllWindowsClosed += EnableControls;
	}

	private void EnableControls()
	{
		controls.Player.Enable();
	}

	private void DisableControls()
	{
		controls.Player.Disable();
	}

	private void OnDestroy()
	{
		WindowController.OnAnyWindowOpened -= DisableControls;
		WindowController.OnAllWindowsClosed -= EnableControls;
		controls.Player.Disable();
		controls.UI.Disable();
	}

	public void OnJump(InputAction.CallbackContext context)
	{
		if (!context.performed) { return; }

		JumpEvent?.Invoke();
	}

	public void OnDodge(InputAction.CallbackContext context)
	{
		if (!context.performed) { return; }

		DodgeEvent?.Invoke();
	}

	public void OnMove(InputAction.CallbackContext context)
	{
		MovementValue = context.ReadValue<Vector2>();
	}

	public void OnLook(InputAction.CallbackContext context)
	{
	}

	public void OnTarget(InputAction.CallbackContext context)
	{
		if (!context.performed) { return; }

		TargetEvent?.Invoke();
	}

	public void OnAttack(InputAction.CallbackContext context)
	{
		if (context.performed)
		{
			IsAttacking = true;
			AttackDownEvent?.Invoke();
		}
		else if (context.canceled)
		{
			IsAttacking = false;
		}
	}

	public void OnBlock(InputAction.CallbackContext context)
	{
		if (context.performed)
		{
			IsBlocking = true;
		}
		else if (context.canceled)
		{
			IsBlocking = false;
		}
	}


	public void OnInteract(InputAction.CallbackContext context)
	{
		if (context.ReadValueAsButton()) ;
		{
			DialogueEvent?.Invoke();
			ShopEvent?.Invoke();
			PickupEvent?.Invoke();
			// InteractEvent?.Invoke();
		}
	}

	public void OnCancel(InputAction.CallbackContext context)
	{
		if (context.performed)
		{
			CancelEvent?.Invoke();
		}
	}

	public void OnInventory(InputAction.CallbackContext context)
	{
		if (context.performed)
		{
			InventoryEvent?.Invoke();
		}
	}

	// public void OnPickup(InputAction.CallbackContext context)
	// {
	// 	if (context.ReadValueAsButton())
	// 	{
	// PickupEvent?.Invoke();
	// 	}
	// }

	// public void OnDialogue(InputAction.CallbackContext context)
	// {
	// 	if (context.ReadValueAsButton()) ;
	// 	{
	// 		DialogueEvent?.Invoke();
	// 	}
	// }
	//
	// public void OnShop(InputAction.CallbackContext context)
	// {
	// 	if (context.ReadValueAsButton()) ;
	// 	{
	// 		ShopEvent?.Invoke();
	// 	}
	// }
}

}

AnimationEventRelay.cs

using UnityEngine;

namespace RPG.Control
{
public class AnimationEventRelay : MonoBehaviour
{
public event System.Action PickupItemEvent;

	void PickupItem()
	{
		PickupItemEvent?.Invoke();
	}
}

}

Any suggestions? :sweat_smile:

This one looks like the PickupTarget isn’t performing the callback.

Remember that the PickupTarget itself needs an event OnPickedUp, and immediately before calling pickup.PickupItem(), it should invoke that event.

PickupFinder.cs
using System.Linq;
using RPG.Core;
using UnityEngine;

namespace RPG.Inventories
{
    public class PickupFinder : RangeFinder<PickupTarget>
    {
        
        protected override void AddTarget(PickupTarget target)
        {
            base.AddTarget(target);
            target.OnPickedUp += RemoveTarget;
        }

        protected override void RemoveTarget(PickupTarget target)
        {
            base.RemoveTarget(target);
            target.OnPickedUp -= RemoveTarget;
        }
    }
}
PickupTarget.cs
using GameDevTV.Inventories;
using RPG.Core;
using UnityEngine;

namespace RPG.Inventories
{
    public class PickupTarget : MonoBehaviour, ITarget
    {
        Pickup pickup;
        private Inventory inventory;

        public event System.Action<PickupTarget> OnPickedUp;

        private void Awake()
        {
            pickup = GetComponent<Pickup>();
            inventory = Inventory.GetPlayerInventory();
        }
        
        public bool IsValid()
        {
            return inventory.HasSpaceFor(pickup.GetItem());
        }

        public void PickupItem()
        {
            OnPickedUp?.Invoke(this);
            pickup.PickupItem();
        }


    }
}

This one I’m not quite sure on… Which event is /Keyboard/e assigned to?

Yes, I have the Event set up in PickupTarget pretty much word for word (The exception being I changed Awake to OnEnable because pickups can be instantiated at runtime and I thought this could be the source of the error),
I even have a comment in the bottom quoting you :sweat_smile:

PickupTarget
using GameDevTV.Inventories;
using RPG.Core;
using UnityEngine;

namespace RPG.Inventories
{
	public class PickupTarget : MonoBehaviour, ITarget
	{
		Pickup pickup;
		private Inventory inventory;

		public event System.Action<PickupTarget> OnPickedUp;

		private void OnEnable()
		{
			pickup = GetComponent<Pickup>();
			inventory = Inventory.GetPlayerInventory();
		}

		public bool IsValid()
		{
			return inventory.HasSpaceFor(pickup.GetItem());
		}

		public void PickupItem()
		{
			OnPickedUp?.Invoke(this);
			pickup.PickupItem();
		}
	}
}

//It's VERY important that we invoke this method BEFORE calling pickup.PickupItem() because
//pickup.PickupItem() will destroy the Pickup entirely and that call will fail
PickupFinder.cs
using System.Linq;
using RPG.Core;
using UnityEngine;

namespace RPG.Inventories
{
	public class PickupFinder : RangeFinder<PickupTarget>
	{
		protected override void AddTarget(PickupTarget target)
		{
			base.AddTarget(target);
			target.OnPickedUp += RemoveTarget;
		}

		protected override void RemoveTarget(PickupTarget target)
		{
			base.RemoveTarget(target);
			target.OnPickedUp -= RemoveTarget;
		}
	}
}

E is bound to “Interact”
image

Which at the moment calls all the events (PlayerFreeLookState):

		stateMachine.InputReader.PickupEvent += InputReader_HandlePickupEvent;
		stateMachine.InputReader.DialogueEvent += InputReader_HandleDialogueEvent;
		stateMachine.InputReader.ShopEvent += InputReader_HandleShopEvent;

I also tried:
changing Interact action from “Button” to “Value>Any”, to see if this is the source but again got the same error.

When the error occours, pressing attack doesn’t do attack (not attacking in the air, not picking up etc) and pops the “Pickup target has been destroyed” error.
I spawned another enemy, and when he was in range i was able to attack him, but attempting to loot his drop popped the errors again.

Let’s add a Debug.Log here to ensure that we’re getting called

protected override void RemoveTarget(PickupTarget target)
{
    Debug.Log($"Removing {target.gameObject} from the list");
    base.RemoveTarget(target);
    target.OnPickedUp-=RemoveTarget;
}

You should see this message whenever you get out of range of a target, or whenever the target is picked up.

Alright, I followed through a bit and I think It’s working now - not entirely sure, though I haven’t been able to reproduce the bug.

I had a script (which I completely forgot about, sorry @Brian_Trotter :pray: :pray: :pray:) that handled picking up gold on trigger enter (filter player tag). I think that it got picked up, but didn’t call the remove event - which caused it to be stuck as current target.

After I added the print,
noticed the bug happens mostly when I fight and kill enemies near the loot.
and that I often had some gold (not a lot though) in my inventory but nothing else. :flushed:

For anyone who might find this useful (as simple as it might be ):
Here is an updated version I created for my OnEnterPickup.cs,

OnEnterPickup.cs
using RPG.Inventories;
using UnityEngine;

[RequireComponent(typeof(PickupTarget))]
public class OnEnterPickup : MonoBehaviour
{
	[SerializeField] private PickupTarget pickupTarget;

	private void OnEnable()
	{
		pickupTarget = GetComponent<PickupTarget>();
	}

	private void OnTriggerEnter(Collider other)
	{
		if (other.CompareTag("Player"))
		{
			pickupTarget.PickupItem();
		}
	}
}

I made it so it calls the method from “PickupTarget” so It doesn’t have to subscribe to the event itself just call the method from the target to handle everything - and still be a component that I can throw on any item I think should be picked up automatically

1 Like

Privacy & Terms