From 4384fb20e38221e386dc28355847e0e760a28882 Mon Sep 17 00:00:00 2001 From: Jussi Rasku Date: Tue, 30 Jan 2024 20:51:34 +0200 Subject: [PATCH 1/3] Modifies and refines pool.gd for Godot 4, improved readme --- README.md | 59 +++++++----- addons/godot-object-pool/icon.png | Bin 2736 -> 0 bytes addons/godot-object-pool/pool.gd | 154 +++++++++++++----------------- project.godot | 20 ++++ 4 files changed, 120 insertions(+), 113 deletions(-) delete mode 100644 addons/godot-object-pool/icon.png create mode 100644 project.godot diff --git a/README.md b/README.md index cdc7312..1640a59 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/addons/godot-object-pool/icon.png b/addons/godot-object-pool/icon.png deleted file mode 100644 index c019ef64a1516db91617204000feb3987f764c9d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2736 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 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 + o.position = Vector2.ZERO + 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_hidden, 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_hidden(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) diff --git a/project.godot b/project.godot new file mode 100644 index 0000000..4cd67ac --- /dev/null +++ b/project.godot @@ -0,0 +1,20 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="godot-object-pool" +run/main_scene="res://example/example.tscn" +config/features=PackedStringArray("4.2", "GL Compatibility") + +[rendering] + +renderer/rendering_method="gl_compatibility" +renderer/rendering_method.mobile="gl_compatibility" From b5ec4edb286fe3f17a3fe653a58550623b7819c0 Mon Sep 17 00:00:00 2001 From: Jussi Rasku Date: Tue, 30 Jan 2024 20:51:46 +0200 Subject: [PATCH 2/3] Adds full example --- example/bullet.gd | 18 ++++++++++++++++++ example/bullet.tscn | 24 ++++++++++++++++++++++++ example/example.gd | 34 ++++++++++++++++++++++++++++++++++ example/example.tscn | 27 +++++++++++++++++++++++++++ example/player.gd | 12 ++++++++++++ 5 files changed, 115 insertions(+) create mode 100644 example/bullet.gd create mode 100644 example/bullet.tscn create mode 100644 example/example.gd create mode 100644 example/example.tscn create mode 100644 example/player.gd diff --git a/example/bullet.gd b/example/bullet.gd new file mode 100644 index 0000000..99204d8 --- /dev/null +++ b/example/bullet.gd @@ -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 diff --git a/example/bullet.tscn b/example/bullet.tscn new file mode 100644 index 0000000..36e6988 --- /dev/null +++ b/example/bullet.tscn @@ -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 diff --git a/example/example.gd b/example/example.gd new file mode 100644 index 0000000..e46055d --- /dev/null +++ b/example/example.gd @@ -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()) diff --git a/example/example.tscn b/example/example.tscn new file mode 100644 index 0000000..15768fe --- /dev/null +++ b/example/example.tscn @@ -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="."] diff --git a/example/player.gd b/example/player.gd new file mode 100644 index 0000000..418738b --- /dev/null +++ b/example/player.gd @@ -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 + From c6776a9737ebb348be654b16d4162e9dd7883cff Mon Sep 17 00:00:00 2001 From: Jussi Rasku Date: Wed, 31 Jan 2024 17:02:16 +0200 Subject: [PATCH 3/3] Remove assumption of Node2D, renamed restock callback --- addons/godot-object-pool/pool.gd | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/addons/godot-object-pool/pool.gd b/addons/godot-object-pool/pool.gd index 589e447..bd689b0 100644 --- a/addons/godot-object-pool/pool.gd +++ b/addons/godot-object-pool/pool.gd @@ -43,7 +43,7 @@ func _init(size_, prefix_, template_): o.set_name(prefix + "_" + str(i)) o.visible = false o.set_process_mode(4) # 4 = PROCESS_MODE_DISABLED - o.hidden.connect(self._on_hidden.bind(o)) + o.hidden.connect(self._on_restock.bind(o)) _dead.push_back(o) func get_prefix(): @@ -68,7 +68,6 @@ func pop_first_dead(): # Turn its processing on and make it visible o.set_process_mode(0) # 0 = PROCESS_MODE_INHERIT o.visible = true - o.position = Vector2.ZERO return o # Get the first alive object. Does not affect / change the object's dead value @@ -80,7 +79,7 @@ func get_first_alive(): # 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_hidden, returns to dead + 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): @@ -90,7 +89,7 @@ func add_to_node(node): node.add_child(o) # Hiding a pool managed object calls this -func _on_hidden(pooled_object): +func _on_restock(pooled_object): # Remove the killed object from the alive pool var n = pooled_object.get_name() _alive.erase(n)