Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 35 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,38 +1,49 @@
# godot-object pool

An object pool for Godot.
An object pool for Godot 4.

# Usage example:
Object pooling in Godot reduces lag by reusing objects instead of constantly
creating and deleting them. It's great for games with lots of temporary objects
like bullets or enemies. You avoid the performance hit from frequent memory
allocation, making your game run smoother.

The pooled objects are initially hidden and their processing is disabled.
When they are popped back to the game, they are made visible and their position
is reset. When they are hidden, they return to the pool. Handy.

PlayerController.gd
# Usage example:

```gdscript
const Pool = preload("res://addons/godot-object-pool/pool.gd")
const GreenBullet = preload("res://com/example/bullets/green_bullet.tscn")
const Bullet = preload("res://example/bullet.tscn")

const BULLET_POOL_SIZE = 60
const BULLET_POOL_PREFIX = "bullet"

onready var bullets = get_node("bullets")
onready var player = get_node("player")
onready var pool = Pool.new(BULLET_POOL_SIZE, BULLET_POOL_PREFIX, GreenBullet)
@onready var pool = Pool.new(BULLET_POOL_SIZE, BULLET_POOL_PREFIX, Bullet)

func _ready():
# Attach pool of objects to the bullets node
pool.add_to_node(bullets)

# Attach the "on_pool_killed" method to the pool's "killed" signal
pool.connect("killed", self, "_on_pool_killed")

set_process_input(true)

func _input(event):
if event.is_action_pressed("ui_select"):
var bullet = pool.get_first_dead()
if bullet: bullet.shoot(player.get_node("weapon_position"), player)

func _on_pool_killed(target):
target.hide()
print("Currently %d objects alive in pool" % pool.get_alive_count())
print("Currently %d objects dead in pool" % pool.get_dead_count())
# Attach pooled objects to the game as children of the root node.
pool.add_to_node(self)

# Called whenever bullet returns to the pool.
pool.restock.connect(_on_pool_restock)

# Print initial status of the pool.
_on_pool_restock()

func _unhandled_input(event):
if event is InputEventMouseButton and event.pressed and\
event.button_index == MOUSE_BUTTON_LEFT:
# Take a bullet from the bool and give it to the player to shoot.
$Player.shoot(pool.pop_first_dead(), event.position)

# After some time the bullet is hidden and it returns to the pool.
# (see bullet.gd) for details.

func _on_pool_restock():
print("Currently %d objects alive in the pool" % pool.get_alive_count())
print("Currently %d objects dead in the pool" % pool.get_dead_count())
```

See the complete example for more details.
Binary file removed addons/godot-object-pool/icon.png
Binary file not shown.
153 changes: 64 additions & 89 deletions addons/godot-object-pool/pool.gd
Original file line number Diff line number Diff line change
@@ -1,129 +1,104 @@
#
# The design/intent of this object pool is to be as immutable as possible from the outside.
# With this in mind, I've attempted to not expose many internal to keep things as simple as possible,
# knowing that nothing actually prevents you from modifying the object.
# With this in mind, an attempt was made to not expose internals and make it as simple as possible.
# Nothing prevents you from modifying the pooled objects. Only their visibility and their `hidden`
# signaling affect the pool.
#
# See README.md for example usage
# See README.md for example usage.
#
#

# Signal emitted when an object managed by the pool is "killed".
# This is called after the pool has handled the killed signal from the object.
signal killed(target)
signal restock(object)

# Prefix to use when adding objects to the scene (becomes "undefined_1, undefined_2, etc")
var prefix setget , get_prefix
var prefix: get = get_prefix

# Pool size on initialization
var size setget , get_size
var size: get = get_size

# Preloaded scene resource
var scene setget , get_scene
# Preloaded scene resource to instantiate into objects and pool
var template: get = get_template

# Dictionary of "alive" objects currently in-use.
# Using a dictionary for fast lookup/deletion
var alive = {}
var _alive = {}

# Array of "dead" objects currently available for use
var dead = []
var _dead = []

# Constructor accepting pool size, prefix and scene
func _init(size_, prefix_, scene_):
# Expands the total pool size by the number of requested objects.
# For example, if passed 2, we will instantiate 2 new objects and add them
# to the dead pool 👽.
func _init(size_, prefix_, template_):
size = int(size_)
prefix = str(prefix_)
scene = scene_
init()

# Expand the total pool size by the number of size objects.
# For example, if passed 2, we will instantiate 2 new objects and add to the dead pool.
func init():
# If scene has not been set, just return
if scene == null:
template = template_

if template == null:
return

for i in range(size):
var s = scene.instance()
s.set_name(prefix + "_" + str(i))
s.connect("killed", self, "_on_killed")
dead.push_back(s)
var o = template.instantiate()
o.set_name(prefix + "_" + str(i))
o.visible = false
o.set_process_mode(4) # 4 = PROCESS_MODE_DISABLED
o.hidden.connect(self._on_restock.bind(o))
_dead.push_back(o)

func get_prefix():
return prefix

func get_size():
return size

func get_scene():
return scene

func get_alive_size():
return alive.size()

func get_dead_size():
return dead.size()
func get_template():
return template
func get_alive_count():
return _alive.size()
func get_dead_count():
return _dead.size()

# Get the first dead object and make it alive, adding the object to the alive pool and removing from dead pool
func get_first_dead():
var ds = dead.size()
if ds > 0:
var o = dead[ds - 1]
if !o.dead: return null

var n = o.get_name()
alive[n] = o
dead.pop_back()
o.dead = false
o.set_pause_mode(0)
return o

return null
func pop_first_dead():
if _dead.is_empty():
return null

var o = _dead.pop_back()
var n = o.get_name()
_alive[n] = o
# Turn its processing on and make it visible
o.set_process_mode(0) # 0 = PROCESS_MODE_INHERIT
o.visible = true
return o

# Get the first alive object. Does not affect / change the object's dead value
func get_first_alive():
if alive.size() > 0:
return alive.values()[0]

return null
if _alive.is_empty():
return null
return _alive.values()[0]

# Convenience method to kill all ALIVE objects managed by the pool
func kill_all():
for i in alive.values():
i.kill()
# Hide all ALIVE objects and, hence, return them to the dead pool
func hide_all_alive():
for a in _alive.values():
a.visible = false # Calls _on_restock, returns to dead

# Attach all objects managed by the pool to the node passed
func add_to_node(node):
for i in alive.values():
node.add_child(i)

for i in dead:
node.add_child(i)

# Convenience method to show all objects managed by the pool
func show():
for i in alive.values():
i.show()

for i in dead:
i.show()

# Convenience method to hide all objects managed by the pool
func hide():
for i in alive.values():
i.hide()

for i in dead:
i.hide()

# Event that all objects should emit so that the pool can manage dead/alive pools
func _on_killed(target):
# Get the name of the target object that was killed
var name = target.get_name()
for a in _alive.values():
node.add_child(a)
for o in _dead:
node.add_child(o)

# Hiding a pool managed object calls this
func _on_restock(pooled_object):
# Remove the killed object from the alive pool
alive.erase(name)
var n = pooled_object.get_name()
_alive.erase(n)

# Add the killed object to the dead pool, now available for use
dead.push_back(target)

target.set_pause_mode(1)

emit_signal("killed", target)
_dead.push_back(pooled_object)

# Disable it to save those precious, precious CPU cycles
pooled_object.set_process_mode(4) # 4 = PROCESS_MODE_DISABLE

# Signal those that are interested of restock events
restock.emit(pooled_object)
18 changes: 18 additions & 0 deletions example/bullet.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
extends Area2D

var velocity : Vector2 = Vector2.ZERO : set = _set_velocity

var state = null

func _set_velocity(v):
velocity = v

# After the timer timeouts, call hide for the bullet.
# Also hiding on a collision would work.
# Hiding the bullet automatically returns it to the pool.
$DropTimer.timeout.connect( func (): self.hide() )
$DropTimer.start()

func _process(delta):
# Makes the bullet fly.
self.position+=velocity*delta
24 changes: 24 additions & 0 deletions example/bullet.tscn
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[gd_scene load_steps=4 format=3 uid="uid://cw1rbf7kgh1mx"]

[ext_resource type="Script" path="res://example/bullet.gd" id="1_i8ei0"]

[sub_resource type="PlaceholderTexture2D" id="PlaceholderTexture2D_6f7bs"]

[sub_resource type="CircleShape2D" id="CircleShape2D_lrbxf"]
radius = 1.0

[node name="Bullet" type="Area2D"]
collision_layer = 2
collision_mask = 2
script = ExtResource("1_i8ei0")
metadata/_edit_group_ = true

[node name="Jacket" type="Sprite2D" parent="."]
scale = Vector2(2, 2)
texture = SubResource("PlaceholderTexture2D_6f7bs")

[node name="HurtyBit" type="CollisionShape2D" parent="."]
shape = SubResource("CircleShape2D_lrbxf")

[node name="DropTimer" type="Timer" parent="."]
one_shot = true
34 changes: 34 additions & 0 deletions example/example.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
extends Node2D

const Pool = preload("res://addons/godot-object-pool/pool.gd")
const Bullet = preload("res://example/bullet.tscn")

const BULLET_POOL_SIZE = 60
const BULLET_POOL_PREFIX = "bullet"

@onready var pool = Pool.new(BULLET_POOL_SIZE, BULLET_POOL_PREFIX, Bullet)

func _ready():
# Attach pooled objects to the game as children of the root node.
pool.add_to_node(self)

# Called whenever bullet returns to the pool
pool.restock.connect(_on_pool_restock)

# Print initial status of the pool
_on_pool_restock(null)

func _unhandled_input(event):
if event is InputEventMouseButton and event.pressed and\
event.button_index == MOUSE_BUTTON_LEFT:
# Take a bullet from the bool and give it to the player to shoot
# Taking it from the pool makes it visible
$Player.shoot(pool.pop_first_dead(), event.position)

# If after some time the bullet is hidden, it returns to the bool
# (see bullet.gd) for details.

func _on_pool_restock(object):
if (object!=null): print("Bullet hidden at "+str(object.position))
print("Currently %d objects alive in the pool" % pool.get_alive_count())
print("Currently %d objects dead in the pool" % pool.get_dead_count())
27 changes: 27 additions & 0 deletions example/example.tscn
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
[gd_scene load_steps=5 format=3 uid="uid://dnjfeodh2t1qh"]

[ext_resource type="Script" path="res://example/player.gd" id="1_1f5k6"]
[ext_resource type="Script" path="res://example/example.gd" id="1_38il6"]

[sub_resource type="PlaceholderTexture2D" id="PlaceholderTexture2D_3tk14"]

[sub_resource type="CapsuleShape2D" id="CapsuleShape2D_k3b7f"]
height = 62.0

[node name="World" type="Node2D"]
script = ExtResource("1_38il6")

[node name="Player" type="Area2D" parent="."]
position = Vector2(605, 314)
script = ExtResource("1_1f5k6")
metadata/_edit_group_ = true

[node name="Skin" type="Sprite2D" parent="Player"]
position = Vector2(0, 1.90735e-06)
scale = Vector2(18.049, 60)
texture = SubResource("PlaceholderTexture2D_3tk14")

[node name="Body" type="CollisionShape2D" parent="Player"]
shape = SubResource("CapsuleShape2D_k3b7f")

[node name="Bullets" type="Node" parent="."]
12 changes: 12 additions & 0 deletions example/player.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
extends Area2D

@export var bullet_speed : float = 250

func shoot(bullet, towards):
if bullet==null:
return #ran out of pooled bullets?

bullet.global_position = self.global_position
# Setting the velocity also sets the bullet to self destruct (see bullet.gd)
bullet.velocity = self.position.direction_to(towards)*bullet_speed

Loading