BlastBullets2D is a library written in C++ for Godot Engine that makes spawning and moving a huge amount of bullets a very efficient operation. Not only performance is increased SIGNIFICANTLY, but you also get the functionality
of SAVING/LOADING the bullets' state through easy to use save()
and load()
functions.
The library comes pre-compiled for:
- Windows (x86_64, arm64)
- Android (x86_64, arm64)
- Linux (x86_64)
- Web
BlastBullets2D should work for IOS and macOS too, but you have to compile the code yourself.
The library is used inside your Godot Engine project just how you use any other Node and Script. This means that you DON'T NEED to know C++ at all to use it! Everything is done by writing code in GDScript that calls the custom C++ functions. This is made possible through Godot's GDExtension technology.
In short, use BlastBullets2D if you are looking for optimized bullets performance in Godot or if you want saving and loading of bullets' state. It is EXTREMELY more optimized compared to using an Area2D
with an AnimationPlayer
.
BlastBullets2D Features
- Efficient rendering by using
MultiMeshInstance2D
- Improved performance by using object pooling
- Improved performance by using only C++ for everything, instead of GDScript
- Saving bullets state
- Loading bullets state
- Debugger for the collision shapes, so you can see what exactly is happening when changing collision related properties
- Speed, max_speed, acceleration
- Rotation speed, rotation max_speed, rotation acceleration
- Custom collision layers and collision masks by providing a bitmask
- Animation by providing multiple textures that switch over a period of time
- Custom texture size and collision shape size
- Custom collision shape offset
- Custom bullet max_life_time
- The ability to provide a custom
Material
andMesh
(if you want to use shaders) - Easy to use
area_entered
andbody_entered
signals similar toArea2D
- The option of providing custom data for a bullet multimesh that is also automatically saved/loaded and also accessed through
area_entered
andbody_entered
. This is very useful when you have data like armor_damage, magic_damage, bullet_type or anything else you can think of that you want for the bullets to hold and apply to an enemy when it's hit
- Download zip and extract it. If however you plan on making changes and compiling the C++ code yourself in the future, then ensure that godot_cpp is also included by running this command:
git clone --recurse-submodules https://github.com/nikoladevelops/godot-blast-bullets-2d.git
- Make sure the folder you got from extracting the zip or cloning is named
BlastBullets2D
. This is important. - Open your Godot game project.
- Create folder named
addons
if it doesn't already exist. - Cut the folder
BlastBullets2D
and paste it inside theaddons
folder. - Close Godot and open the project again.
- Add a
BulletFactory2D
node to your scene tree. The BulletFactory's job is to spawn bullets
(I suggest creating an Autoload/Singleton so you can spawn bullets from any script). - Create a script.
- Inside the script create a
BlockBulletsData2D
and set up its properties according to the documentation. Example:
var data:BlockBulletsData2D = BlockBulletsData2D.new()
data.transforms = getNewMarkerTransforms() # a custom function that returns an array of transforms
data.textures = allTextures # an array of preloaded textures
var speed_data:Array[BulletSpeedData] = BulletSpeedData.generate_random_data(2, 100,200,250,250,500,1000);
data.all_bullet_speed_data=speed_data
data.collision_layer = BlockBulletsData2D.calculate_bitmask([1])
data.collision_mask = BlockBulletsData2D.calculate_bitmask([3])
data.texture_size = Vector2(64,64)
- Use the factory's
spawnBlockBullets2D()
function every time you want to spawn bullets and provide aBlockBulletsData2D
as an argument. Example:
factory.spawnBlockBullets2D(data)
You can view a Demo Project to see how the library is used
The mandatory properties that you need to set for BlockBulletsData2D
are: transforms
and all_bullet_speed_data
.
The transforms
property requires an array of Transform2D
, where each entry determines the position and rotation of a bullet. The rotation of each transform determines the direction of the corresponding bullet, but only if the amount of transforms is the same amount of BulletSpeedData
instances provided in all_bullet_speed_data
.
all_bullet_speed_data
expects an array of BulletSpeedData
, each defining the properties acceleration
, speed
, and max_speed
. You can create this array easily using the provided static method BulletSpeedData.generate_random_data()
. Ensure that the number of BulletSpeedData
instances matches the number of Transform2D
entries to maintain individual bullet directions. Otherwise, all bullets will share the same direction determined by data.block_rotation_radians
, moving as a block for better performance.
BlockBulletsData2D
textures
: Array containing textures. If more than one texture is provided,max_change_texture_time
will be used to periodically change the texture.texture_size
: Size of the texture (used if no mesh is provided). Default:Vector2(32,32)
.texture_rotation_radians
: Rotation of the texture in radians. Use if the texture is not rotated properly. Example: If you want to rotate the texture 90 degrees more then you would do90*PI/180
current_texture_index
: Index of the starting texture in the array. Default:0
.max_change_texture_time
: Time before changing the texture to the next one in the array. Default:0.3f
.is_texture_rotation_permanent
: Determines if texture rotation is permanent. By default the texture's rotation changes based on the bullet's rotation. If for some reason you want the texture's rotation to never be affected then set this to true.Default:false
.
transforms
: Array determining rotation and position of each bullet. The rotation of eachTransform2D
determines the direction in which the corresponding bullet will travel BUT ONLY ifuse_block_rotation_radians
is set tofalse
AND if the amount ofBulletSpeedData
inall_bullet_speed_data
is the same as the amount ofTransform2D
provided insidetransforms
(meaning you haveBulletSpeedData
for every bullet).block_rotation_radians
: This is a rotation that determines the direction in which ALL bullets will travel as a block. It is used only whenuse_block_rotation_radians
is set totrue
. Default:0.0f
.use_block_rotation_radians
: Iftrue
, forces all bullets to move as a block and only the firstBulletSpeedData
insideall_bullet_speed_data
is used. SIGNIFICANTLY BOOSTS PERFORMANCE but the bullets will be moving with the same speed/max_speed/acceleration, so they may not look as good. The direction in which ALL bullets will move is determined byblock_rotation_radians
. Default:false
.all_bullet_speed_data
: Array providing speed data for each bullet. Use the static methodBulletSpeedData.generate_random_data()
to generate an array ofBulletSpeedData
easily.
all_bullet_rotation_data
: Optional array providing rotation data for each bullet. Populate this array withBulletRotationData
if you want your bullets to spin. Give only a singleBulletRotationData
if you want ALL your bullets to spin with the same speed/max_speed/acceleration. You should give the same amount ofBulletRotationData
as the size oftransforms
array if you want each bullet to spin with individual speed/max_speed/acceleration. Use the static methodBulletRotationData.generate_random_data()
to easily generateBulletRotationData
. If you don't provide at least 1BulletRotationData
OR if the amount of data is not the same as the amount ofTransform2D
insidetransforms
then all provided data will be ignored and your bullets WILL NOT rotate/spin.rotate_only_textures
: By default only the textures are being rotated whenall_bullet_rotation_data
is populated. If for some reason you want the collision shapes to also rotate with the textures then set this tofalse
(this will decrease performance). Default:true
.
collision_layer
: Bitmask for collision layer. Use the static methodBlockBulletsData2D.calculate_bitmask()
to easily get a bitmask. NEVER set this to 0 or negative number.collision_mask
: Bitmask for collision mask. Use the static methodBlockBulletsData2D.calculate_bitmask()
to easily get a bitmask. NEVER set this to 0 or negative number.collision_shape_size
: Size of collision shape (rectangle). Default:Vector2(5,5)
. If you want your collision shape to be bigger/smaller then change this.collision_shape_offset
: Offset of collision shape. If you want your collision shape to be positioned away from the center of the texture then change this.monitorable
: Iftrue
, enablesStaticBody2D
detection. I suggest you DO NOT use this. It will make it possible for your bullets to detectStaticBody2D
, but it DECREASES PERFORMANCE A LOT. A good workaround is to always have anArea2D
on your static bodies, that hasmonitorable
set totrue
andmonitoring
set tofalse
. ThisArea2D
will act as the place where bullets can hit. Note that even thoughmonitorable
is set tofalse
by default, the bullets will still be able to interact withCharacterBody2D
andRigidBody2D
bodies, the exception is onlyStaticBody2D
, so follow my advice.bullets_custom_data
: Additional data for bullets. If you want your bullets to have damage or anything else specific then you do this -> Create a class script that extends Resource -> Put@export
variables inside like damage/armor_damage or whatever else you need (the@export
keyword is extremely important otherwise the data won't be saved!) -> Create a new instance of your class (example: MyCustomResource.new()) -> populate the properties -> pass it inside here. Congrats, now you can access your custom_data from thearea_entered
andbody_entered
function callbacks inside thefactory
!
Example of a Custom Resource class:
class_name DamageData
extends Resource
@export var base_damage:int
@export var armor_damage:int
@export var magic_damage:int
max_life_time
: Duration before bullets are disabled/dissapear. Default:2.0f
.material
: Custom material (maybe you want to use shaders?).mesh
: Custom mesh (if provided,texture_size
property is ignored, so handle resizing of the texture inside your shader).
int calculate_bitmask(numbers:Array[int]) static
: Method to acquire a bitmask from an array of integers. NEVER pass 0 or negative numbers, it will cause issues.
BulletFactory2D
-
physics_space
: The physics space where the bullets' collision shapes are interacting with the world. You don't really need to touch this unless you know what you are doing. -
is_debugger_enabled
: Determines whether the collision shape debugger is enabled or not. When exporting your game or testing performance make sure that this is set tofalse
, because it tanks performance. Use only when you want to debug your collision shapes (what happens when you increase a collision shape's size and see where the shape is positioned relative to the texture).
void spawnBlockBullets2D(spawn_data:BlockBulletsData2D)
: Spawn bullets with the provided data.SaveDataBulletFactory2D save()
: Saves the bullets' state and returns aSaveDataBulletFactory2D
resource that you can save to a file. When theSaveDataBulletFactory2D
resource is finished being populated with bullet state data, thefinished_saving
signal is emitted.void load(new_data:SaveDataBulletFactory2D)
: Loads the bullets' state from aSaveDataBulletFactory2D
resource. When the loading of bullets to the scene tree finishes, thefinished_loading
signal is emitted.void clear_all_bullets()
: Clears all bullets from the object pool and also from the scene. Always call this method usingcall_deffered()
to avoid your game crashing. When clearing finishes, thefinished_clearing
signal is emitted.
Watch this quick tutorial on Custom Resources to understand more of what this following code does and how you can implement your own custom SAVING/LOADING logic for the rest of your game: Godot Custom Resources Tutorial and read the Godot Resources Documentation
Simple implementation of saving and loading:
@onready var factory:BulletFactory2D = $MyBulletFactory # get a reference to the factory node
var savePath:String = OS.get_user_data_dir() + "/test.tres"; # use the .res extension if you want it saved as binary data (I suggest looking into encryption for actual security, so the user won't be able to change damage/speed values and so on..)
# When the save button is pressed
func _on_save_btn_pressed():
var data:SaveDataBulletFactory2D = factory.save(); # Get the bullets' state
var result = ResourceSaver.save(data, savePath) # Saves the data to savePath and return whether it was successful
if result == OK:
# saving is successful
else:
# saving failed, handle the error
# When the load button is pressed
func _on_load_btn_pressed():
var data:SaveDataBulletFactory2D = ResourceLoader.load(savePath) # get the data that is saved at savePath
if data != null:
factory.call_deferred("clear_all_bullets") # clear all old bullets from the level
factory.call_deferred("load", data) # load the bullets from the save file
else:
# handle error, data was null/ an error occured
Note that when saving your game's state by using save()
, you have to ensure that the user won't spam click your save/load buttons which may cause invalid data to be saved to the SaveDataBulletFactory2D
that gets returned.
To avoid such bugs, ensure that when saving/loading you lock the GUI menu's buttons that are used for saving/loading until that finishes (the finished_loading
signal is very helpful in that case).
-
area_entered(enemy_area: Object, bullets_custom_data: Resource, bullet_global_position: Vector2)
: Theenemy_area
is theArea2D
that got hit with the bullet. CheckBlockBullets2D
documentation to see how to set upbullets_custom_data
that can store damage and other additional properties. Thebullet_global_position
is the last position the bullet had before dissapearing, so you can use it to spawn particles at the same place. -
body_entered(enemy_body: Object, bullets_custom_data: Resource, bullet_global_position: Vector2)
: Only active ifmonitorable
of the bullets is set totrue
. CheckBlockBulletsData2D
for more info. Theenemy_body
is the body that got hit with the bullet. CheckBlockBulletsData2D
documentation to see how to set upbullets_custom_data
that can store damage and other additional properties. Thebullet_global_position
is the last position the bullet had before dissapearing, so you can use it to spawn particles at the same place.
Ensure that the enemy Area2D
has monitorable
set to true
and also that the collision_layer
and collision_mask
for both the Area2D
and the bullets are configured correctly.
The same applies for bodies too.
Example of implemented callbacks that use area_entered
and body_entered
:
func _on_bullet_factory_2d_area_entered(enemy_area, bullets_custom_data:Resource, bullet_global_position:Vector2):
if bullets_custom_data is DamageData: # maybe you have bullets that have other bullets_custom_data types and you have individual logic for each?
var actualEnemy = enemy_area.get_parent() # if the Area2D was added just how I recommended to AVOID setting monitorable to true, you can get the parent of the area which will be the static body you want to damage
#if (actualEnemy is CustomEnemyType)
# apply bonus damage or don't apply magic damage or any other complex logic
# maybe check if actualEnemy.immunityArray() contains bullets_custom_data.type or something like that, you can do pretty much anything
actualEnemy.take_damage(bullets_custom_data.armor_damage)
func _on_bullet_factory_2d_body_entered(enemy_body, bullets_custom_data:Resource, bullet_global_position:Vector2):
if bullets_custom_data is DamageData:
enemy_body.take_damage(bullets_custom_data.armor_damage)
finished_saving
: Emitted whensave()
method is done populating theSaveDataBulletFactory2D
.finished_loading
: Emitted when all bullets fromSaveDataBulletFactory2D
were added to the scene tree.finished_clearing
: Emitted when all bullets were cleared/deleted from the object pool and from the scene tree.
- Object pooling is automatic, bullets are NEVER DELETED, instead they stay completely DISABLED in the scene tree until they are about to be re-used.
- When saving, only the active bullets are being saved, which means that bullets that are in the pool are NEVER SAVED.
- If you are switching game levels and you think that having too many disabled bullets impacts your performance in a bad way, instead of helping to increase your FPS, you can use the
clear_all_bullets()
function, which will clear ALL ACTIVE BULLETS in the scene tree AND ALL DISABLED BULLETS that are in the object pool. - In some cases it might be beneficial to first populate the object pool before starting your game level. You can use a
for
loop and thespawnBlockBullets2D()
function for this task. Use the same exact data but with very littlemax_life_time
, so that the bullets can instantly enter the object pool. Consider displaying a loading screen while the object pool is being populated. An EXTREMELY IMPORTANT thing to know is that the object pool is actually poolingMultiMeshInstance2D
nodes and the whole pooling mechanism relies on thetransforms.size()
ofBlockBulletsData2D
(meaning the amount of bullets a single multimesh has). If you populate your pool withMultiMeshInstance2D
nodes that havetransforms.size()
equal to 30 (meaning eachMultiMeshInstance2D
node holds 30 bullets and let's say you spawn 550 of them to populate the pool), but in your game you frequently spawn bullets that are made out of only 20Transform2D
and you RARELY spawn bullets withtransforms.size()
equal to 30 exactly, then all those 550MultiMeshInstance2D
nodes won't be re-used until you use the spawn function with aBlockBulletsData2D
that has atransforms
array with.size()
equal to EXACTLY 30. In short, use thespawnBlockBullets2D()
withBlockBulletsData2D
that hastransforms.size()
equal to the most spawned bullets amount at once (if your enemies and player always very frequently spawn 20 bullets at once, then you would ensure thetransforms
array holds 20Transform2D
, before spam calling thespawnBlockBullets2D()
to populate the object pool).
Compilation instructions
- Download source code with included godot_cpp submodule.
git clone --recurse-submodules https://github.com/nikoladevelops/godot-blast-bullets-2d.git
python -m pip install scons
- Go to main folder where SContrsuct.py file is located. Open your command terminal (Example: cmd on Windows) in the same directory then type one of these depending on the platform you are targeting (if you receive an error it means you don't have the required toolchain to compile for the platform you are targeting, so do some research on what you're missing):
scons platform=windows arch=x86_64 target=template_debug
scons platform=windows arch=x86_64 target=template_release
scons platform=windows arch=arm64 target=template_debug
scons platform=windows arch=arm64 target=template_release
scons platform=linux arch=x86_64 target=template_debug
scons platform=linux arch=x86_64 target=template_release
Ensure you have an Android SDK (you can download Android Studio and get all the things you need from there). Here is some useful documentation Compiling for Android
scons platform=android arch=x86_64 target=template_debug ANDROID_HOME=C:\Users\Admin\AppData\Local\Android\Sdk
scons platform=android arch=x86_64 target=template_release ANDROID_HOME=C:\Users\Admin\AppData\Local\Android\Sdk
scons platform=android arch=arm64 target=template_debug ANDROID_HOME=C:\Users\Admin\AppData\Local\Android\Sdk
scons platform=android arch=arm64 target=template_release ANDROID_HOME=C:\Users\Admin\AppData\Local\Android\Sdk
You need emscripten SDK. Put emsdk and emscripten's location inside environment variable Path. Before trying to compile for web, each time you open your command terminal you need to run this
emsdk activate latest
After than run each of these:
scons platform=web target=template_debug
scons platform=web target=template_release
Due to me not having these OS-es I can only give you short instructions on what to look for and which files you need to edit.
First of all this whole library relies on GDExtension so it has a .gdextension file with path locations of the binaries it needs to load. See godot_cpp's .gdextension file vs mine. Also notice how their SConstruct file differs from mine and add the missing logic: theirs vs mine. Research on which toolchain you need for IOS and for macOS, download them and then run the same scons commands, but for your desired platform and other desired arguments. See the official GDExtension Documentation or search for some tutorials online. Sadly GDExtenstion is not well documented, so you might spend some time searching. I recommend joining Godot's Discord Server it has a gdnative-gdextension channel so you might find some help there.