"Invalid access to property or key 'custom_property' on a base object of type 'Nil'"

Godot Version

Godot 4.6.1

Issue

I’m trying to utilize exported variables to make my project easy to modify and add stuff for in the future. If possible, I want to be able to assign all of the data in the Inspector to quickly make a new instance of an object.

However, when I change my Resource structure just a little bit, it starts to cause the error shown above, and I can’t find out why my resources stop importing properly and how to fix it.

Details

  • The data causing this error:
  1. BeastCore (Resource): Contains immutable data, made to be saved to define the very core of the instance.
  2. BeastData (Resource): Contains mutable data, that helps make each monster instance unique (like stats). Has an exported variable that holds the immutable data.
  3. Beast (Node): A custom-defined extension of Node that, when initialized (the initialization requires Monster Data as an argument), contains all the relevant data for other scripts/nodes to access. I am trying to remove this node, now that I know the data can be properly initialized within the Resource.
  4. Battle Entity (New Resource): An empty resource class. I’m trying to make BeastData and another resource extend this resource, for organization. I want my enemy’s exported enemy_data variable to only accept a Battle Entity, so that I’m limited to selecting extended Resources that are made to work with that specific node.
  5. Enemy (Node): Holds the data and assigns the variables accordingly, based on what exported variable is returned. I am giving that variable BeastData.
  • When the exact project folder is copied over from my desktop, the project works just fine.
  • Any changes that I make to the resource structure will cause Godot to never show the imported data in the Stack Frames, even though I have it correctly set within the exported variable.
  • Issues with resources not properly imported only started recently, when I was having some issues with managing a bare git repository I use to hold my project between computers. I’m not sure if this can cause something like this to happen with Godot?

  1. The Enemy Script (Original):
extends Node2D

@export var enemy_data: Resource

var active_beast: Beast
## Assigns itself the beast data, depending on if the enemy is a trainer with multiple beasts, or just a wild beast.
var enemy_beast: BeastData

func _ready() -> void:
	# Get the data either the beast as a wild beast, or the first beast in a trainer's party for a trainer battle.
	if enemy_data is Trainer:
		enemy_beast = enemy_data.party[0]
	elif enemy_data is BeastData:
		enemy_beast = enemy_data
	# If the resource type is unrecognized, do not continue loading a sprite.
	else:
		printerr("Unrecognized resource type. Can only load a Trainer for a trainer battle, or a Beast for a wild beast battle.")
		get_tree().quit(1)
	
	# Set the beast's data.
	set_active_beast(enemy_beast)

@onready var health_bar_size = $BattleUI/HealthBar.size.x

func set_active_beast(beast_data: BeastData) -> void:
	if active_beast: active_beast.queue_free()
	active_beast = Beast.new(beast_data)
	$BeastSprite.texture = active_beast.sprite
	$BattleUI/NameTag.set_text(active_beast.beast_name + " lv. " + str(active_beast.level))
  1. The Beast Script (Original):
class_name Beast extends Node

var beast_name: String

var sprite: Texture

var max_health: int

var current_health: int

var level: int

var speed: int

var learned_attacks: Array[Attacks]

var beast_data: BeastData

func _init(new_beast: BeastData) -> void:
	beast_data = new_beast

	beast_name = new_beast.nickname if new_beast.nickname else new_beast.beast_core.name

	sprite = new_beast.beast_core.sprite

	max_health = new_beast.max_health

	current_health = new_beast.current_health

	level = new_beast.level

	speed = new_beast.speed

	learned_attacks = new_beast.learned_attacks
  1. Beast Data Script (Original):
## The beast's data. Wraps around the core to provide mutable data like stats, health, and experience, to make the beast whole.
class_name BeastData extends Resource

## The very bare minimum data that defines the beast. This data stays the same between any of the same beast.
@export var beast_core: BeastCore

## The beast's max health.
@export var max_health: int = 100

## The beast's current health.
var current_health: int = max_health

## The beast's level.
@export var level: int
## The beast's speed. Determines which beast goes first.
@export var speed: int

## The 4 attacks this beast currently knows.
@export var learned_attacks: Array[Attacks]

@export var nickname: String
  1. The Enemy Script (Changed):
extends Node2D

@export var enemy_data: BattleEntity

var active_beast: BeastData

func _ready() -> void:
	## Assigns itself the beast data, depending on if the enemy is a trainer with multiple beasts, or just a wild beast.
	var enemy_beast: BeastData

	# Get the data either the beast as a wild beast, or the first beast in a trainer's party for a trainer battle.
	if enemy_data is Trainer:
		enemy_beast = enemy_data.party[0]
	elif enemy_data is BeastData:
		enemy_beast = enemy_data
	# If the resource type is unrecognized, do not continue loading a sprite.
	else:
		printerr("Unrecognized resource type. Can only load a Trainer for a trainer battle, or a Beast for a wild beast battle.")
		get_tree().quit(1)
	
	# Set the beast's data.
	set_active_beast(enemy_beast)

@onready var health_bar_size = $BattleUI/HealthBar.size.x

func set_active_beast(beast_data: BeastData) -> void:
	active_beast = beast_data
	$BeastSprite.texture = active_beast.sprite
	$BattleUI/NameTag.set_text(active_beast.name + " lv. " + str(active_beast.level))
  1. Beast Data (Changed, Replacing Beast):
## The beast's data. Wraps around the core to provide mutable data like stats, health, and experience, to make the beast whole.
class_name BeastData extends BattleEntity

## The very bare minimum data that defines the beast. This data stays the same between any of the same beast.
@export var beast_core: BeastCore

## The beast's max health.
@export var max_health: int = 100

## The beast's current health.
var current_health: int = max_health

## The beast's level.
@export var level: int
## The beast's speed. Determines which beast goes first.
@export var speed: int

## The 4 attacks this beast currently knows.
@export var learned_attacks: Array[Attacks]

@export var nickname: String
var name: String

func _init(new_beast_core: BeastCore, new_max_health: int = 100, new_current_health: int = new_max_health, new_level: int = 1, new_speed: int = 1, new_learned_attacks: Array[Attacks] = [], new_name: String = ""):
	beast_core = new_beast_core
	name = nickname if nickname else beast_core.name if beast_core.name else new_name
	max_health = new_max_health
	current_health = new_current_health
	level = new_level
	speed = new_speed
	learned_attacks = new_learned_attacks

## Sets the health for the specified beast.
func set_health(health_bar: Node, max_bar_size: float, new_health: int) -> void:
	if new_health >= max_health: current_health = max_health
	else:
		current_health = new_health
		
	health_bar.size.x = (current_health * max_bar_size)/max_health
  1. Battle Entity Script (New):
class_name BattleEntity extends Resource

If you are experienced with Godot and/or Resources, may you assist with this? I don’t know if this is an error I caused and failed to understand, or if there actually is something wrong with my Godot. I created an account just to ask this, I’ve literally never been stumped so badly by an error before.

Thank you!

Don’t use _init, especially for Node types. _init will try to run before @exports, it is the least stable state to run code in.

The only plausible extends to override _init is Object or RefCounted and still only if you have a very good reason to do so.

1 Like

That is very helpful to know, thank you! I do see in the documentation that _init is called before the exported variables are assigned, which definitely made it weird that the way I built it was working.

After trying a bunch of different things, I think the best thing I can do is restore from a previous commit, build something else, and come back to remaking this later.

Thank you for your help!

After messing around with a previous version of my project, I found out what the problem was.

The Problem:

Making changes to the class name and what it inherits from through an external editor (VSCodium in this case) was not being recognized by Godot; It would then tell me that it couldn’t find the data I inputted, even though the code was definitely correct. I found this out when I changed my Entrance class to inherit from WorldEntity instead of Node2D; Only when I did this, would my project start giving me an error saying something like Entrance is not a recognized class when a previously working script would try to check if certain nodes were Entrances.

While it is true that exported values are only assigned after _init is called, it wasn’t a problem in this scenario because the first beast was initialized when the enemy script called it in _ready(), after the exported variables attached to enemy.gd were already assigned.

The Solution

I’m not sure if I was having this issue because I had the external editor improperly configured… But for now, I’m going back to Godot’s Script Editor, with an addon that makes it more like an IDE. Changes I make to the class structure in there will not break the game anymore!

Happy coding! B)