• Godot HelpCSharp
  • "Cannot access a disposed object" error on a node that should not be disposed

This problem is oddly specific so bear with me. While learning Godot, I'm making a simple turn-based RPG game. When the player triggers a random encounter, they are switched to the Battle scene. Either the player or enemy will start the battle (right now it's just a coin flip), and if it's the player's turn, a menu allowing the player to select their action will show.

If the battle randomly starts on the player's turn, there is no error. If the battle randomly starts on the enemy turn AND this is the first time the battle scene has been visited since launch, there is no error.

It's upon entering a random encounter a subsequent time and starting on the enemy's turn that I get an error where one of the action buttons is now 'disposed'.

I have a few guesses as to what may be occurring, but to provide all of the details first:

In my battle scene, my root node has a script (BattleManager) that handles the turn order. Here's what that script looks like:

using Godot;
using System;
using System.Collections.Generic;
using System.Linq;

public class BattleManager : Control
{
	
	BattlePartyContainer partyContainer;
	[Export]
	NodePath partyContainerPath;
	BattleActionContainer actionContainer;
	[Export]
	NodePath actionContainerPath;
	Panel actionPanel;
	[Export]
	NodePath actionPanelPath;

	BattleEnemyContainer enemyContainer;
	[Export]
	NodePath enemyContainerPath;

	BattleEntitySelect entitySelect;
	[Export]
	NodePath entitySelectPath;
	BattleState currentState;
	int turnNumber = 0;
	PartyMember[] _partyMembers = new PartyMember[4];
	Enemy[] _enemies = new Enemy[3];
	Random random = new Random();
	float startingChance = 0.5f;

	public override void _Ready()
	{
		partyContainer = GetNode<BattlePartyContainer>(partyContainerPath);
		actionContainer = GetNode<BattleActionContainer>(actionContainerPath);
		enemyContainer = GetNode<BattleEnemyContainer>(enemyContainerPath);
		actionPanel = GetNode<Panel>(actionPanelPath);
		entitySelect = GetNode<BattleEntitySelect>(entitySelectPath);

		BattleEnemyContainer.AttackParty += (damage) => TakeDamage(damage);

		SetBattleEntities();

		RandomizeStart();
		InitTurn();
	}
		void RandomizeStart()
	{
		double randomNumber = random.NextDouble();
		currentState = randomNumber < startingChance ? BattleState.PlayerTurn : BattleState.EnemyTurn;
	}

	void SetBattleEntities()
	{
		_partyMembers = GameManager.Instance.PlayerData.PartyMembers;
		_enemies = enemyContainer.GetEnemyArray();
	}

	void InitTurn()
	{	
		if (currentState == BattleState.PlayerTurn)
		{
			actionPanel.Modulate = new Color(1f, 1f, 1f, 1f);
			actionContainer.SetActionText("Double Slash");
			ActionButton.UseAction += () => UseAction();
			GD.Print($"It is currently {_partyMembers[turnNumber].EntityName}'s turn!");
			partyContainer.SetSlotSelected(_partyMembers[turnNumber]);
		}
		else
		{
			ActionButton.UseAction -= () => UseAction();
			actionPanel.Modulate = new Color(1f, 1f, 1f, 0f);
			GD.Print($"It is currently {_enemies[turnNumber].EntityName}'s turn!");
			enemyContainer.SetAction(turnNumber);
		}	
	}

	void SetNextTurn()
	{
		if (currentState == BattleState.PlayerTurn)
		{
			do
			{
				turnNumber = (turnNumber + 1) % _partyMembers.Count();
			} while (_partyMembers[turnNumber] == null || _partyMembers[turnNumber].Hp <= 0);			
			
			currentState = (turnNumber == 0) ? BattleState.EnemyTurn : currentState;
		}
		else
		{
			do
			{
				turnNumber = (turnNumber + 1) % _enemies.Count();
			} while (_enemies[turnNumber] == null || _enemies[turnNumber].Hp <= 0);
					
			currentState = (turnNumber == 0) ? BattleState.PlayerTurn : currentState;
		}
		InitTurn();
	}

	private void UseAction()
	{
		GD.Print($"This should trigger the use of an action for {_partyMembers[turnNumber].EntityName}!");
		//ShowEntitySelect(_partyMembers[turnNumber].WeaponSlot.ActionType);
	}

	private void TakeDamage(int damage)
	{	
		List<int> availableIndexes = new List<int>();
		
		for (int i = 0; i < _partyMembers.Count(); i++)
		{
			if (_partyMembers[i] != null && _partyMembers[i].Hp > 0)
			{
				availableIndexes.Add(i);
			}
		}

		double randomNumber = random.NextDouble();
		int randomIndex = random.Next(0, availableIndexes.Count);

		GameManager.Instance.ChangePartyMemberHP(randomIndex, damage);
		SetBattleEntities();
		SetNextTurn();
	}

	void ShowEntitySelect(GearAction state)
	{
		List<Entity> entityList = new List<Entity>();

		switch (state)
		{
			case GearAction.Attack:
				foreach (Enemy enemy in _enemies)
				{
					if (enemy.Hp > 0)
					{
						entityList.Add(enemy);
					}		
				}
			break;

			case GearAction.Heal:
				foreach (PartyMember member in _partyMembers)
				{
					if (member.Hp > 0)
					{
						entityList.Add(member);
					}		
				}
			break;

			case GearAction.Revive:
				foreach (PartyMember member in _partyMembers)
				{
					if (member.Hp == 0)
					{
						entityList.Add(member);
					}		
				}
			break;
		}
		
		entitySelect.InitEnemyButtons(entityList);
	}
}

Now, trust me, I don't like that I have a million exports and GetNode()s either. Down the line, I'll probably turn those child containers into their own 'scenes' so I can just instantiate them from this manager itself. But before that can happen, I have bigger fish to fry.

I've been receiving an error that I cannot access a disposed object (one of the action buttons which are children of the action panel), but my issue is that nowhere in my code do I dispose (by QueueFree() or otherwise) said object. If you were curious why I used actionPanel.Modulate = new Color(1f, 1f, 1f, 1f); instead of setting the panels to be visible/invisible, it's because, at one point, I assumed making the parent panel invisible was somehow the culprit. That proved not to be the case although it did oddly enough fix another issue where the first child button of ActionPanel was not being focused.

Anyway! The error in question is:

E 0:00:20.819   IntPtr Godot.Object.GetPtr(Godot.Object ): System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'ActionButton'.
  <C++ Error>   Unhandled exception
  <C++ Source>  /root/godot/modules/mono/glue/GodotSharp/GodotSharp/Core/Object.base.cs:43 @ IntPtr Godot.Object.GetPtr(Godot.Object )()
  <Stack Trace> Object.base.cs:43 @ IntPtr Godot.Object.GetPtr(Godot.Object )()
                Button.cs:195 @ void Godot.Button.SetText(System.String )()
                Button.cs:58 @ void Godot.Button.set_Text(System.String )()
                BattleActionContainer.cs:5 @ void BattleActionContainer.SetActionText(System.String )()
                BattleManager.cs:65 @ void BattleManager.InitTurn()()
                BattleManager.cs:99 @ void BattleManager.SetNextTurn()()
                BattleManager.cs:125 @ void BattleManager.TakeDamage(Int32 )()
                BattleManager.cs:41 @ void BattleManager.<_Ready>b__16_0(Int32 )()
                :0 @ ()

Here's the script 'BattleActionContainer' and the class it extends (something, something prefer composition over inheritance. Yeah, I feel you.)

using Godot;

public class BattleActionContainer : ButtonFocusManager
{
	public void SetActionText(string text) => buttons[0].Text = text;
}
using Godot;
using System;
using System.Collections.Generic;

public class ButtonFocusManager : VBoxContainer
{
	protected List<Button> buttons = new List<Button>();
	protected int focusedButtonIndex = 0;
	[Export]
	Texture arrowIcon;
	public override void _Ready()
	{
		foreach (Button button in GetChildren())
		{
			buttons.Add(button);
			button.Connect("focus_entered", this, "OnButtonFocusEntered");
			button.Connect("focus_exited", this, "OnButtonFocusExited");
		}

		buttons[focusedButtonIndex].GrabFocus();
	}

	protected void OnButtonFocusEntered()
	{
		focusedButtonIndex = buttons.IndexOf(GetFocusOwner() as Button);
		
		buttons[focusedButtonIndex].GetChild<TextureRect>(0).Texture = arrowIcon;
	}

	protected void OnButtonFocusExited()
	{
		buttons[focusedButtonIndex].GetChild<TextureRect>(0).Texture = null;
	}
}

All this script really does is place an arrow icon next to the focused button, but I had to extend it since nodes can only have one script, and I needed to have some way to specifically set the action name for the current party member (some goofy "Tornado Slam" nonsense or whatever).

Now for my guesses:

  • I'm not super knowledge of what Godot actually does with data between scenes. Is it possible the previous action button list is still in memory from the last time the Battle scene had been entered but that now points to Button objects that have now been disposed? I previously assumed that a fresh instance of ActionPanel would be created every time this scene is entered (and _Ready subsequently called), but is this not correct?

  • I do subscribe and unsubscribe from ActionButton's UseAction event when the menu becomes accessible or inaccessible, but since this error occurs upon a subsequent visit to the Battle scene, is it possible that this event is persisting between scenes? I did, as a test, also unsubscribe from UseAction when the scene switches, but that didn't stop the error from occurring.

  • If a parent object is made invisible or, I guess in this case, if its Alpha value is set to 0, would that make child nodes inaccessible? I know that sounds silly, but toggling visibility is one of the features I added right before this error occurred, and again, I am not knowledgeable of how Godot frees memory so I figured it's worth an ask.

  • When I attempted to put a Try...Catch in SetActionText, the game still crashed and gave an error. That is where the trace leaves off, but am I maybe missing and something is happening before then that I'm not noticing.

Other that that, I've got nothing! Let me know if I can provide any other details.

Edit: Forgive the formatting. I've tried to move around the ` markers, but certain snippets just won't appear as a code block.
Edit again: Tack så mycket min gode herre xyz! The formatting is lovely now.

  • xyz replied to this.
  • RienKT I was working on the assumption that it would be disposed along with everything else.

    Static class members are not disposed with class instances (objects) as they are not associated with them. That's the whole point of making something static. It's a language thing. Nothing to do with Godot per se.

    RienKT For proper code formatting enclose the code block between lines containing only ~~~
    Try to make a minimal project that reproduces the problem.

      xyz

      Thank you for the formatting help! It's lovely now.

      I'll start recreating a test version of the project. My dilemma is that this error does seem connected to a very specific case so I'm uncertain how I would isolate it in a way that would still locate the issue.

      On their own, many of these scripts do function as intended. ButtonFocusManager, for example, is used across all menus that need an arrow on the focused button. And the fact that the error doesn't simply if the enemy side starts first but if it's a subsequent time the player reaches the Battle scene AND the enemy goes first is making me wonder if this is related to how data persists between scene switches.

      • xyz replied to this.

        RienKT A project can always be stripped to a bare minimum that only reproduces the problem. You can post such project here and someone will likely take a look. But often in the process of stripping down the cause may become apparent.

        Btw, on the first glance, your class names are very abstract. From my experience this is often a warning sign that project's architecture design might make it unwieldy in the long run.

          xyz

          I managed to recreate the error. So, in this simpler scene, I have a BattleManager that starts a random turn based on a coin flip. If it's the player's turn, the action menu is visible, the manager subscribes to the ActionButton's UseAction event, and ActionContainer sets the action's text (in this case, to Tornado Slam! because why not?)

          using Godot;
          using System;
          
          public class BattleManager : Control
          {
          	[Export]
          	NodePath ActionContainerPath;
          	ActionContainer ActionContainer;
          
          	[Export]
          	NodePath ActionPanelPath;
          	Panel ActionPanel;
          
          	Random random = new Random();
          
          	public override void _Ready()
          	{
          		ActionContainer = GetNode<ActionContainer>(ActionContainerPath);
          		ActionPanel = GetNode<Panel>(ActionPanelPath);
          
          		StartTurn();
          	}
          
          	bool CoinFlip()
          	{
          		int number = random.Next(0, 100);
          		return number <= 50;
          	}
          
          	void StartTurn()
          	{
          		if (CoinFlip())
          		{
          			GD.Print("It's the player's turn.");
          			ActionPanel.Visible = true;
          			ActionButton.UseAction += () => Attack();
          			ActionContainer.SetActionText("Tornado Slam");
          		}
          		else
          		{
          			GD.Print("It's the enemy's turn.");
          			ActionButton.UseAction -= () => Attack();
          			ActionPanel.Visible = false;
          
          			StartTurn();
          		}
          	}
          
          	void Attack()
          	{
          		GD.Print("Yay! You slaughtered it!");
          		StartTurn();
          	}
          
          }

          On an unrelated note, does anyone know a better way to get references to child nodes? I really am not a fan of NodePath exports, but I hate hard-coding string paths even more. My guess is that I'm supposed to make these their own 'scenes' and instantiate them from the parent node? Is that the standard way or is there something else that's more common for Godot?

          Anyway! This is the error message I receive. It is the same message as before. When I load this isolated text game and switch to the Battle scene, it always works fine the first time. If, after leaving the Battle scene and returning, I click on the ActionButton, it gives this error:

          E 0:00:04.557   IntPtr Godot.Object.GetPtr(Godot.Object ): System.ObjectDisposedException: Cannot access a disposed object.
          Object name: 'ActionButton'.
            <C++ Error>   Unhandled exception
            <C++ Source>  /root/godot/modules/mono/glue/GodotSharp/GodotSharp/Core/Object.base.cs:43 @ IntPtr Godot.Object.GetPtr(Godot.Object )()
            <Stack Trace> Object.base.cs:43 @ IntPtr Godot.Object.GetPtr(Godot.Object )()
                          Button.cs:195 @ void Godot.Button.SetText(System.String )()
                          Button.cs:58 @ void Godot.Button.set_Text(System.String )()
                          ActionContainer.cs:17 @ void ActionContainer.SetActionText(System.String )()
                          BattleManager.cs:37 @ void BattleManager.StartTurn()()
                          BattleManager.cs:52 @ void BattleManager.Attack()()
                          BattleManager.cs:36 @ void BattleManager.<StartTurn>b__7_0()()
                          :0 @ ()
                          ActionButton.cs:9 @ void ActionButton._Pressed()()

          While it's slightly different that clicking the button causes the issue vs simply loading the scene before, I think that's as close of a recreation as I'll probably get. My problem is still... I don't know how to fix it.

          Despite the trace ending at SetActionText(), I still feel like the problem has to instead be related to the C# event. For reference, here is the ActionContainer script which, as far as I can tell, should always be able to get a reference to its first child button element:

          using Godot;
          using System.Collections.Generic;
          
          public class ActionContainer : VBoxContainer
          {
          	List<Button> actionButtons = new List<Button>();
          	
          	public override void _Ready()
          	{
          		foreach (Button button in GetChildren())
          		{
          			actionButtons.Add(button);
          		}
          	}
          
          	public void SetActionText(string text) => actionButtons[0].Text = text;
          }

          As for the C# event itself, it is working the first time the Battle scene is entered. It's when I leave the Battle scene and return that I receive the error. My guess is that events need to be handled differently when it comes to scene transitions, but I don't know the correct practice.

          • xyz replied to this.

            RienKT Can you upload the minimal reproduction project?
            Btw, what is ActionButton? I don't see it declared anywhere in the class code you posted.

              xyz

              My last post has unfortunately been waiting for approval for half a day, but here's the Github URL: https://github.com/kettukakku/BattleTest.git

              As for ActionButton, it just invokes the C# event:

              using Godot;
              using System;
              
              public class ActionButton : Button
              {
              	public static event Action UseAction;
              
              	public override void _Pressed()
              	{
              		UseAction?.Invoke();
              	}
              }
              • xyz replied to this.

                RienKT You made quite a mess there 😃

                First, your SceneManager is instantiated twice at startup. It's set as autoload as well as the main startup scene. So once you run the project, you get two instances of it running. This is guaranteed to cause bugs in the future, given that you like to use static members. If you need it as an autoload, use a different scene as your startup scene.
                Second, your BattleManager::StartTurn() is recursive, which is a very weird way to handle turns. Try to get rid of that recursion because it could also become a source of future bugs.

                However bad the above things may be, they're actually not the cause of the disposed object problem.

                Here's what it is. In ActionButton you declare UseAction as a static System.Action event. Making it static means that it will not be destroyed when the scene containing this button is destroyed. So all delegates you add to it in BattleManager::StartTurn() will survive re-creation of the battle manager scene. When this event fires in the next instance of the scene, it will try to call all delegates you've added so far, including the ones referring to the previous instance of the scene. Since that instance is gone, the "cannot access disposed object" exception is thrown.

                As a general suggestion - use Godot's signals instead of generic C# events.

                  xyz

                  The SceneManager and BattleManager in this minimal test are not how scenes and battles are managed in the actual project (the BattleManager is provided in my first post and would be much more helpful to receive critique on). The GameManager singleton in the main project does QueueFree() any duplicate instances but that one also starts on a MainMenu scene anyway. I just didn't see a point in setting up an actual turn system when the only purpose of this was to quickly recreate the disposed object issue.

                  The state of my minimal test project aside, thank you for locating the issue. I didn't realize the event would persist through scenes. I was working on the assumption that it would be disposed along with everything else.

                  As for C# vs Godot's signals, I did originally use Godot signals instead but found the editor clunkier to work with. Either way, I won't trouble you with my mess any longer. I'll keep reading up on events and see what I come up with.

                  • xyz replied to this.

                    RienKT I was working on the assumption that it would be disposed along with everything else.

                    Static class members are not disposed with class instances (objects) as they are not associated with them. That's the whole point of making something static. It's a language thing. Nothing to do with Godot per se.

                      xyz

                      Alas. I think I adopted that tactic (public static event Action whatever) from a Unity tutorial from a while back and failed to fully understand it.

                        RienKT It shows 😉.
                        Invest some time into fully figuring out how Godot's signals work. I think you'll find them to be quite straightforward and easy to use. And they are much more in tune with Godot's design philosophy.

                        RienKT I adopted that tactic (public static event Action whatever)

                        Would that be a stactic?