- Edited
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.