Godot Native Bullets
Efficiently spawn and move high amounts of objects like bullets for bullet hells, particles and more.
This is a GDNative plugin, compatible with Godot 3.4.x and up.
Not compatible with Godot 4.x.
Pre-built binaries are provided for:
- Windows x86-64
- Linux x86-64
- macOS Universal (x86-64 + arm64)
Features
- Spawn and move high amounts of bullets.
- Configure multiple types of bullets, each with its own behaviour.
- Set custom properties to categories of bullets or to any single bullet.
- Customize bullets using shaders.
- Configure bullets with a collision shape, layer and mask of your choice.
- Toggle collision detection on or off for each type of bullet.
- Set the pool size and z index for every bullet type.
- Extend the plugin using C++.
Step by step
Installation
- Copy the addons/native_bullets folder in your addons folder.
- Navigate to Project -> Project Settings -> Plugin and enable the Native Bullets plugin.
For best performance, toggle use_bvh
off in Project Settings -> Physics -> 2d.
BulletKit creation
The first thing to do is create a BulletKit resource and choose how bullets will appear and behave tweaking its properties.
-
Create a new empty resource and assign one of the scripts you can find in the
addons/native_bullets/kits
folder to it. In this example, we'll assign thebasic_bullet_kit.gdns
script. New properties will appear in the resource inspector. -
To get started fill the
texture
property with any texture you have. -
As the
material
, you can use a new material resource using theanimated_shader.gdshader
you can find in the utils folder. This shader takes care of animating your bullets if you specify more than 1 frame in its parameters. -
For now, turn off
collisions_enabled
, turnuse_viewport_as_active_rect
on, turnrotate
off and setunique_modulate_component
toNone
. See the Reference section to learn more.
This BulletKit resource is now ready to be used!
Bullets node
In order to spawn bullets in the scene, a Bullets node is needed. You can add it as an autoload or as a normal node in the scene.
The Bullets node has to be configured to choose which kinds of bullets will be able to spawn, their maximum amount and their z indices.
-
Create a new Bullets node.
-
Choose how many types of bullets this node will be able to spawn. Increase
bullet_types_amount
to 1 for now. -
A new section of the inspector will appear below. Unfold the 0 entry inside the Bullet Types section. Here, drag & drop the BulletKit resource you created earlier to let the node know that you'll want to spawn the bullet described in it!
-
Choose the maximum amount of bullets setting the
pool_size
property and the theirz_index
. 3000 and 1 will be ok.
Nice! Now you can set this node as an autoload for easier access.
Spawn bullets
Now the last step is to spawn a lot of those bullets. Let's create a script.
export(Resource) var bullet_kit
func _process(delta):
if Input.is_key_pressed(KEY_SPACE):
shoot()
if Input.is_key_pressed(KEY_RIGHT):
rotation += PI * delta
if Input.is_key_pressed(KEY_LEFT):
rotation -= PI * delta
func shoot():
var bullet_velocity = Vector2(cos(global_rotation), sin(global_rotation)) * 30.0
# Define which properties will be set to the newly spawned bullet.
# The bullet will be spawned in the same position as this node,
# travelling in the direction defined by its rotation.
var properties = {
"transform": Transform2D(global_rotation, global_position),
"velocity": bullet_velocity
}
# Spawn a bullet using the selected BulletKit and setting the properties defined above.
# Bullets is an autoload.
Bullets.spawn_bullet(bullet_kit, properties)
Here you go! Now attach this script to one of your 2D nodes, start the game, press space to shoot and right/left to change the bullets direction!
More examples
Bullets spawning
Spawn bullets using a Bullets node.
In this example Bullets is an autoload.
export(float) var bullets_speed = 50.0
func shoot():
var bullet_kit = preload("res://path/to/following_bullet_kit.tres")
# The bullet will be spawned in the same position as this node,
# travelling in the direction defined by its rotation.
var bullet_velocity = Vector2(cos(global_rotation), sin(global_rotation)) * bullets_speed
# Follow the first node inside the Player group.
var target_node = get_tree().get_nodes_in_group("Player")[0]
# Define which properties will be set to the newly spawned bullet.
var properties = {
"target_node": target_node,
"transform": Transform2D(global_rotation, global_position),
"velocity": bullet_velocity
}
# Spawn a bullet using the selected BulletKit and setting the properties defined above.
Bullets.spawn_bullet(bullet_kit, properties)
Bullets handling
You can manipulate and set properties of bullets even after you spawned them.
In this example Bullets is an autoload.
export(float) var bullets_speed = 50.0
func shoot():
var bullet_kit = preload("res://path/to/basic_bullet_kit.tres")
# Use `obtain_bullet` to receive an opaque BulletID object.
var bullet_id = Bullets.obtain_bullet(bullet_kit)
# You can then use `bullet_id' to set and get properties.
Bullets.set_bullet_property(bullet_id, "transform", Transform2D(global_rotation, global_position))
Bullets.set_bullet_property(bullet_id, "velocity", Vector2(cos(global_rotation), sin(global_rotation)) * bullets_speed)
Collision detection
When a bullet collides with a body or an area, the best way to get the data of the colliding bullet is to connect to the area_shape_entered
signal.
In this example Bullets is an autoload.
func _on_area_shape_entered(area_id, _area, area_shape, _local_shape):
if not Bullets.is_bullet_existing(area_id, area_shape):
# The colliding area is not a bullet, returning.
return
# Get a BulletID from the area_shape passed in by the engine.
var bullet_id = Bullets.get_bullet_from_shape(area_id, area_shape)
# Get bullet properties, transform, velocity, lifetime etc.
var bullet_transform = Bullets.get_bullet_property(bullet_id, "transform")
# If you previously set a custom Dictionary containing the `damage` key as the data property, you can retrieve it.
var bullet_damage = Bullet.get_bullet_property(bullet_id, "data").damage
# You can also retrieve the BulletKit that generated the bullet and get/set its properties.
var kit_collision_shape = Bullets.get_kit_from_bullet(bullet_id).collision_shape
# Remove the bullet, call_deferred is necessary because the Physics2DServer is in its flushing state during callbacks.
Bullets.call_deferred("release_bullet", bullet_id)
Reference
The plugin has two main components, explained below:
@ BulletKit
This is the resource used to configure each type of bullet.
Every BulletKit spawns its own bullet instances that support different kinds of properties.
The plugin comes with 4 base variants that can be customized via the editor:
BasicBulletKit
This is the most basic BulletKit, makes bullets move, generally at constant speed.
It's configurable with:
texture
: controls what texture is sent to the bullet material.material
: the material used to render each bullet.collisions_enabled
: enables or disables collision detection, turning it off increases performances.collision_layer_
: the collision layer to use during collision detection. Visible only ifcollisions_enabled
is on.collision_mask
: the collision mask to use during collision detection. Visible only ifcollisions_enabled
is on.collision_shape
: the CollisionShape to use during collision detection. Visible only ifcollisions_enabled
is on.use_viewport_as_active_rect
: if enabled, uses the current viewport to detect whether a bullet should be deleted.active_rect
: the rect outside of which the bullets get deleted. Visible only ifuse_viewport_as_active_rect
if off.rotate
: controls whether the bullets automatically rotate based on their direction of travel.unique_modulate_component
: controls which modulate component in the material will be used as a unique value for each bullet instance. This can be used to offset bullets animation frames by unique amounts inside shaders and it's needed due to Godot not supporting material instance properties in 3.x.data
: custom data you can assign to the BulletKit.
Bullets spawned by a BasicBulletKit have those properties:
item_rid
: the bullet canvas item RID, used internally. Read-only.cycle
: the number of times the bullet was recycled, used internally. Read-only.shape_index
: the index of the collision shape used by this bullet, used internally. Read-only.transform
: the Transform2D used to position and rotate the bullet.velocity
: the Vector2 that will be used to update the bullet position.lifetime
: how much time the bullet has been alive.data
: custom data you can assign to any bullet.
FollowingBulletKit
This BulletKit lets you define a target node for each bullet and a turning speed used to chase a it around.
It's configurable with:
texture
: controls what texture is sent to the bullet material.bullets_turning_speed
: the turning speed with which the bullets will rotate towards the target node.material
: the material used to render each bullet.collisions_enabled
: enables or disables collision detection, turning it off increases performances.collision_layer_
: the collision layer to use during collision detection. Visible only ifcollisions_enabled
is on.collision_mask
: the collision mask to use during collision detection. Visible only ifcollisions_enabled
is on.collision_shape
: the CollisionShape to use during collision detection. Visible only ifcollisions_enabled
is on.use_viewport_as_active_rect
: if enabled, uses the current viewport to detect whether a bullet should be deleted.active_rect
: the rect outside of which the bullets get deleted. Visible only ifuse_viewport_as_active_rect
if off.rotate
: controls whether the bullets automatically rotate based on their direction of travel.unique_modulate_component
: controls which modulate component in the material will be used as a unique value for each bullet instance. This can be used to offset bullets animation frames by unique amounts inside shaders and it's needed due to Godot not supporting material instance properties in 3.x.data
: custom data you can assign to the BulletKit.
Bullets spawned by a FollowingBulletKit have those properties:
target_node
: the Node2D the bullet will chase.item_rid
: the bullet canvas item RID, used internally. Read-only.cycle
: the number of times the bullet was recycled, used internally. Read-only.shape_index
: the index of the collision shape used by this bullet, used internally. Read-only.transform
: the Transform2D used to position and rotate the bullet.velocity
: the Vector2 that will be used to update the bullet position.lifetime
: how much time the bullet has been alive.data
: custom data you can assign to any bullet.
DynamicBulletKit
Lets you define bullets that change their behaviour based on their lifetime.
It's configurable with:
texture
: controls what texture is sent to the bullet material.lifetime_curves_span
: the reference lifespan used by the curves to determine their current value.lifetime_curves_loop
: controls whether the curves will loop around whenlifetime
goes pastlifetime_curves_span
.speed_multiplier_over_lifetime
: controls the bullet speed multiplying its velocity magnitude by the value defined in this curve, based on the bullet lifetime.rotation_offset_over_lifetime
: controls the bullet rotation offsetting its initial rotation by the value in radians defined in this curve, based on the bullet lifetime.material
: the material used to render each bullet.collisions_enabled
: enables or disables collision detection, turning it off increases performances.collision_layer_
: the collision layer to use during collision detection. Visible only ifcollisions_enabled
is on.collision_mask
: the collision mask to use during collision detection. Visible only ifcollisions_enabled
is on.collision_shape
: the CollisionShape to use during collision detection. Visible only ifcollisions_enabled
is on.use_viewport_as_active_rect
: if enabled, uses the current viewport to detect whether a bullet should be deleted.active_rect
: the rect outside of which the bullets get deleted. Visible only ifuse_viewport_as_active_rect
if off.rotate
: controls whether the bullets automatically rotate based on their direction of travel.unique_modulate_component
: controls which modulate component in the material will be used as a unique value for each bullet instance. This can be used to offset bullets animation frames by unique amounts inside shaders and it's needed due to Godot not supporting material instance properties in 3.x.data
: custom data you can assign to the BulletKit.
Bullets spawned by a DynamicBulletKit have those properties:
transform
: the Transform2D used to position and rotate the bullet.starting_transform
: the baseline Transform2D, curves will apply their changes based on this transform. This is automatically set when you manually edittransform
.velocity
: the Vector2 that will be used to update the bullet position.starting_speed
: the baseline speed, curves will apply their changes based on this speed value. This is automatically set when you manually editvelocity
.item_rid
: the bullet canvas item RID, used internally. Read-only.cycle
: the number of times the bullet was recycled, used internally. Read-only.shape_index
: the index of the collision shape used by this bullet, used internally. Read-only.lifetime
: how much time the bullet has been alive.data
: custom data you can assign to any bullet.
FollowingDynamicBulletKit
Lets you define bullets that change their chasing behaviour based on their lifetime, their distance to the target, or their angle to the target.
It's configurable with:
texture
: controls what texture is sent to the bullet material.lifetime_curves_span
: the reference lifespan used by the curves to determine their current value. Only used when modeBased On Lifetime
is selected on a curve.distance_curves_span
: the reference max distance used by the curves to determine their current value. Only used when modeBased On Target Distance
is selected on a curve.lifetime_curves_loop
: controls whether the curves will loop around whenlifetime
goes pastlifetime_curves_span
. Only used when modeBased On Lifetime
is selected on a curve.speed_control_mode
: specifies the mode the speed multiplier curve will determine its value:Based On Lifetime
: the curve x axis will map to the lifetime of the bullet, from 0 tolifetime_curves_span
.Based On Target Distance
: the curve x axis will map to the distance to the target, from 0 todistance_curves_span
.Based on Angle To Target
: the curve x axis will map to the angle between the bulletvelocity
Vector2 and the Vector2 pointing from the bullet to the target node, from 0 to PI.
speed_multiplier
: controls the bullet speed multiplying its velocity magnitude by the value defined in this curve, based on whathever is set inspeed_control_mode
.turning_speed_control_mode
: specifies the mode the turning speed curve will determine its value:Based On Lifetime
: the curve x axis will map to the lifetime of the bullet, from 0 tolifetime_curves_span
.Based On Target Distance
: the curve x axis will map to the distance to the target, from 0 todistance_curves_span
.Based on Angle To Target
: the curve x axis will map to the angle between the bulletvelocity
Vector2 and the Vector2 pointing from the bullet to the target node, from 0 to PI.
turning_speed
: controls the bullet turning speed towards the target node, based on whathever is set inturning_speed_control_mode
.material
: the material used to render each bullet.collisions_enabled
: enables or disables collision detection, turning it off increases performances.collision_layer_
: the collision layer to use during collision detection. Visible only ifcollisions_enabled
is on.collision_mask
: the collision mask to use during collision detection. Visible only ifcollisions_enabled
is on.collision_shape
: the CollisionShape to use during collision detection. Visible only ifcollisions_enabled
is on.use_viewport_as_active_rect
: if enabled, uses the current viewport to detect whether a bullet should be deleted.active_rect
: the rect outside of which the bullets get deleted. Visible only ifuse_viewport_as_active_rect
if off.rotate
: controls whether the bullets automatically rotate based on their direction of travel.unique_modulate_component
: controls which modulate component in the material will be used as a unique value for each bullet instance. This can be used to offset bullets animation frames by unique amounts inside shaders and it's needed due to Godot not supporting material instance properties in 3.x.data
: custom data you can assign to the BulletKit.
Bullets spawned by a FollowingDynamicBulletKit have those properties:
target_node
: the Node2D the bullet will chase.velocity
: the Vector2 that will be used to update the bullet position.starting_speed
: the baseline speed, curves will apply their changes based on this speed value. This is automatically set when you manually editvelocity
.item_rid
: the bullet canvas item RID, used internally. Read-only.cycle
: the number of times the bullet was recycled, used internally. Read-only.shape_index
: the index of the collision shape used by this bullet, used internally. Read-only.transform
: the Transform2D used to position and rotate the bullet.lifetime
: how much time the bullet has been alive.data
: custom data you can assign to any bullet.
@ Bullets
Bullets is the node used to spawn bullets into the scene.
It can be configured through the editor setting which kinds of bullets will be used, the pool sizes, the z indices and the collision layer/mask.
# Spawns a bullet using the passed BulletKit and setting the properties contained in the `properties` dictionary.
# Returns whether a bullet has been spawned successfully.
spawn_bullet(bullet_kit : BulletKit, properties : Dictionary) -> bool
# Spawns and returns an opaque ID of a bullet using the passed BulletKit.
obtain_bullet(bullet_kit : BulletKit) -> BulletID
# Attempts to delete the bullet referenced by the passed `bullet_id`. Returns whether the removal was successful.
release_bullet(bullet_id : BulletID) -> bool
# Returns whether the bullet referenced by `bullet_id` is still alive and valid.
is_bullet_valid(bullet_id : BulletID) -> bool
# Returns whether `kit` has been configured and is ready to be used in this Bullets instance.
is_kit_valid(kit : BulletKit) -> bool
# Returns the number of currently available bullets for the `kit` BulletKit.
get_available_bullets(kit : BulletKit) -> int
# Returns the number of currently active bullets for the `kit` BulletKit.
get_active_bullets(kit : BulletKit) -> int
# Returns the number of pooled bullets for the `kit` BulletKit.
get_pool_size(kit : BulletKit) -> int
# Returns the z index of the bulltes generated by the `kit` BulletKit.
get_z_index(kit : BulletKit) -> int
# Returns the total number of currently available bullets.
get_total_available_bullets() -> int
# Returns the total number of currently active bullets.
get_total_active_bullets() -> int
# Returns whether `area_rid` and `area_shape` represent a valid and active bullet.
is_bullet_existing(area_rid : RID, area_shape : int) -> BulletID
# Returns the opaque ID of a bullet based on its area RID and its shape index.
get_bullet_from_shape(area_rid : RID, area_shape : int) -> BulletID
# Returns the BulletKit that defined the bullet referenced by the passed `bullet_id`.
get_kit_from_bullet(bullet_id : BulletID) -> BulletKit
# Sets the indicated property of the bullet referenced by `bullet_id`.
set_bullet_property(bullet_id : BulletID, property : String, value : Variant) -> void
# Returns the indicated property of the bullet referenced by `bullet_id`.
get_bullet_property(bullet_id : BulletID, property : String) -> Variant
Compiling and extending the plugin
The plugin can be extended with new BulletKits that are more suitable to your specific use cases.
To do so, you have to download the entire repository together with submodules, write some C++ code and recompile the plugin.
git clone --recursive https://github.com/samdze/godot-native-bullets-plugin
New BulletKits can be added creating a new header file inside the src/kits directory. Here's an example.
// src/kits/custom_following_bullet_kit.h #ifndef CUSTOM_FOLLOWING_BULLET_KIT_H #define CUSTOM_FOLLOWING_BULLET_KIT_H #include <Texture.hpp> #include <PackedScene.hpp> #include <Node2D.hpp> #include <SceneTree.hpp> #include <cmath> #include "../bullet_kit.h" using namespace godot; // Bullet definition. // This is necessary only if your BulletKit needs custom efficiently accessible bullet properties. class CustomFollowingBullet : public Bullet { // Godot requires you to add this macro to make this class work properly. GODOT_CLASS(CustomFollowingBullet, Bullet) public: Node2D* target_node = nullptr; // the _init method must be defined. void _init() {} // Custom setter and getter, not needed. void set_target_node(Node2D* node) { target_node = node; } Node2D* get_target_node() { return target_node; } static void _register_methods() { // Registering an Object reference property with GODOT_PROPERTY_HINT_RESOURCE_TYPE and hint_string is just // a way to tell the editor plugin the type of the property, so that it can be viewed in the BulletKit inspector. register_property( "target_node", &CustomFollowingBullet::set_target_node, &CustomFollowingBullet::get_target_node, nullptr, GODOT_METHOD_RPC_MODE_DISABLED, GODOT_PROPERTY_USAGE_NO_INSTANCE_STATE, GODOT_PROPERTY_HINT_RESOURCE_TYPE, "Node2D"); } }; // Bullet kit definition. // Add your own properties, methods and exports. class CustomFollowingBulletKit : public BulletKit { // Godot requires you to add this macro to make this class work properly. GODOT_CLASS(CustomFollowingBulletKit, BulletKit) public: // Use this macro to configure this bullet kit. // Pass the BulletsPool type that will be used as the argument. BULLET_KIT(CustomFollowingBulletsPool) Ref texture; float bullets_turning_speed = 1. 0f; static void _register_methods() { register_property >( "texture", &CustomFollowingBulletKit::texture, Ref (), GODOT_METHOD_RPC_MODE_DISABLED, GODOT_PROPERTY_USAGE_DEFAULT, GODOT_PROPERTY_HINT_RESOURCE_TYPE, "Texture"); register_property float>( "bullets_turning_speed", &CustomFollowingBulletKit::bullets_turning_speed, 1. 0f, GODOT_METHOD_RPC_MODE_DISABLED, GODOT_PROPERTY_USAGE_DEFAULT, GODOT_PROPERTY_HINT_RANGE, "0.0,128.0"); // Add this macro at the end of the _register_methods() method. // Pass this BulletKit type and the used Bullet type as arguments. BULLET_KIT_REGISTRATION(CustomFollowingBulletKit, CustomFollowingBullet) } }; // Bullets pool definition. // This is the class that will handle the logic linked to your custom BulletKit. // It must extend AbstractBulletsPool. class CustomFollowingBulletsPool : public AbstractBulletsPool { void _init_bullet(CustomFollowingBullet* bullet) { // Initialize your bullet however you like. } void _enable_bullet(CustomFollowingBullet* bullet) { // Runs when a bullet is obtained from the pool and is being enabled. // Reset the bullet lifetime. bullet-> lifetime = 0. 0f; Rect2 texture_rect = Rect2(-kit-> texture-> get_size() / 2. 0f, kit-> texture-> get_size()); RID texture_rid = kit-> texture-> get_rid(); // Configure the bullet to draw the kit texture each frame. VisualServer::get_singleton()-> canvas_item_add_texture_rect(bullet-> item_rid, texture_rect, texture_rid); } void _disable_bullet(CustomFollowingBullet* bullet) { // Runs when a bullet is being removed from the scene. } bool _process_bullet(CustomFollowingBullet* bullet, float delta) { // Runs each frame for each bullet, here goes your update logic. if(bullet-> target_node != nullptr) { // Find the rotation to the target node. Vector2 to_target = bullet-> target_node-> get_global_position() - bullet-> transform. get_origin(); float rotation_to_target = bullet-> velocity. angle_to(to_target); float rotation_value = Math::min(kit-> bullets_turning_speed * delta, std::abs(rotation_to_target)); // Apply the rotation, capped to the max turning speed. bullet-> velocity = bullet-> velocity. rotated( Math::sign(rotation_to_target) * rotation_value); } // Apply velocity. bullet-> transform. set_origin(bullet-> transform. get_origin() + bullet-> velocity * delta); if(!active_rect. has_point(bullet-> transform. get_origin())) { // Return true if the bullet should be deleted. return true; } // Rotate the bullet based on its velocity if "rotate" is enabled. if(kit-> rotate) { bullet-> transform. set_rotation(bullet-> velocity. angle()); } // Bullet is still alive, increase its lifetime. bullet-> lifetime += delta; // Return false if the bullet should not be deleted yet. return false; } }; // Add this macro at the end of the file to automatically implement a few needed utilities. // Pass the BulletKit type and the BulletsPool type as arguments. BULLET_KIT_IMPLEMENTATION(CustomFollowingBulletKit, CustomFollowingBulletsPool) # endif
Next, register you Godot classes inside the gdlibrary.cpp
file.
// src/gdlibrary.cpp #include "bullets.h" ... ... #include "kits/custom_following_bullet_kit.h" ... ... extern "C" void GDN_EXPORT godot_nativescript_init(void *handle) { ... ... // Custom Bullet Kits. godot::register_class(); godot::register_class (); }
Compile the bindings and the plugin for your selected platform.
cd addons/native_bullets/godot-cpp
scons platform=windows target=release generate_bindings=yes -j4
cd ..
scons platform=windows target=release