Implement NodePool optimization with NOTIFY_PREDELETE for automatic node recycling
This PR implements automatic node recycling for NodePool using Godot's NOTIFY_PREDELETE notification system, allowing developers to use queue_free() normally while nodes are automatically returned to the pool instead of being destroyed.
Problem
Currently, NodePool requires explicit code to return nodes to the pool, making game code pool-aware and complicating memory management:
# Before: Manual pool management required
var node = pool.next()
# ... use node ...
# Developer must remember to manually return to pool
if node.get_parent():
node.get_parent().remove_child(node)
pool.return_node(node) # Manual cleanup
Solution
The new PooledNode base class leverages NOTIFY_PREDELETE to intercept queue_free() calls and automatically return nodes to their pool:
# After: Transparent automatic recycling
var node = pool.next() # Returns a PooledNode
# ... use node ...
node.queue_free() # Automatically returns to pool!
Key Features
Transparent Operation: Game code can use queue_free() as normal without pool awareness
Automatic Recycling: Nodes are intercepted before deletion and returned to pool
Memory Optimization: Reduces allocations and garbage collection pressure
Backward Compatible: Existing NodePool usage continues to work unchanged
Robust Error Handling: Prevents double-returns and handles edge cases safely
Implementation Details
-
PooledNode Base Class: Overrides
_notification()to catchNOTIFY_PREDELETE, callscancel_free(), and returns to pool -
Enhanced NodePool: Sets
owner_poolreferences and providesreturn_to_pool()method with proper state management -
State Reset System: Calls
reset_for_pool()when nodes are returned to ensure clean reuse - Pool Limit Management: Maintains size limits by replacing oldest nodes when full
Usage Example
# Create a pooled decal system
class_name PooledBulletHole extends PooledNode
@export var lifespan: float = 30.0
var timer: Timer
func _ready():
timer = Timer.new()
timer.wait_time = lifespan
timer.timeout.connect(queue_free) # Will auto-return to pool
add_child(timer)
func setup_hole(position: Vector3, texture: Texture2D):
global_position = position
# Setup decal properties...
timer.start()
func reset_for_pool():
# Clean state when returned to pool
timer.stop()
global_position = Vector3.ZERO
# Usage in game code
var pool = NodePool.new()
pool.target_node = PooledBulletHole.new()
func create_bullet_hole(hit_pos: Vector3):
var hole = pool.next() # Gets pooled node
hole.setup_hole(hit_pos, bullet_texture)
# Hole automatically returns to pool after lifespan - no cleanup needed!
Benefits for Game Development
- Simplified Code: No manual pool management in game logic
- Performance: Reduced memory allocations for frequently created/destroyed objects
- Maintainability: Pool behavior is encapsulated in the node class
-
Flexibility: Works with timers, collision detection, or manual
queue_free()calls - Debug Friendly: Nodes behave normally but get recycled transparently
This optimization is particularly valuable for systems like bullet holes, particle effects, projectiles, and UI elements that are frequently created and destroyed during gameplay.
Testing
Includes comprehensive unit tests and integration tests to verify automatic recycling, pool limits, state reset, and edge case handling. The implementation has been designed to be robust and fail gracefully.
Original prompt
This section details on the original issue you should resolve
<issue_title>Prevent NodePool nodes being frees</issue_title> <issue_description># Node Pool Optimization using NOTIFY_PREDELETE
Description
We can optimize our node pool implementation by leveraging Godot's notification system to automatically catch nodes before they're freed.
Implementation Details
Instead of requiring explicit code to return nodes to the pool, we can:
- Override the
_notification(what)method in pooled nodes- Check for
what == NOTIFY_PREDELETEandis_queued_for_deletion()- Call
cancel_free()and return the node to the poolBenefits
- Game code doesn't need to be aware of the pool
- Developers can use
queue_free()as normal- Nodes are automatically recycled instead of being destroyed
- Reduces memory allocations and improves performance
Technical Notes
This pattern allows for transparent pooling without changing existing game code that uses
queue_free().</issue_description>Comments on the Issue (you are @copilot in this section)
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.