Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Intro + Q&A

Bevy💋Godot (pronounced “Bevy Kissing Godot”) is a framework that adds Bevy’s ECS on top of Godot.

This project is HEAAAVILY inspired by Godot Bevy, and it would almost certainly not exist if it didn’t come up with the idea first. On the off chance someone besides me is reading this, I would highly recommend trying that project first.

I chose to create my own library for three major reasons:

  • I want the ability to add Bevy components on Godot nodes in the Godot editor.

  • My lack of knoweldge on the internals of Bevy and Godot-Bevy made me paranoid I may encounter a bug or unexpected behavior I could not fix myself.

  • Godot-Bevy automates a bit too much for me. I want to create my own ExtensionLibrary, I want to access Godot nodes via a NonSend, and some third thing I’m too lazy to think of but I’m sure there’s something idk it’s been a couple months since I tinkered with it.

So I decided I could fix these things by creating my own Bevy ECS in Godot project from the ground up.

Why Bevy ECS?

Because I like using components in gamedev. And I like Rust. Idk it fun.

Why is it named that?

One may assume the name exists to reduce confusion with Godot-Bevy and distinguish this from inevitable future Bevy + Godot projects, but actually I don’t care about those things at all and it’s because I ship two male personifications of Bevy and Godot in a toxic yaoi relationship where they kiss passionately.

Add Bevy components in the Godot editor?

Using a Godot addon, the Godot’s Nodes’ inspectors now have a component list you can add your Bevy components to. Simply add the KissingComponent derive to a Bevy component to make it available!

You can use #[export] to allow properties to be modified in the editor (with the exact same options from gdext!).

Check out the Components section for more details.

use godot::prelude::*;
use bevy::prelude::*;
use bevy_kissing_godot::prelude::*;

#[derive(Component, KissingComponent)]
struct Grid {
	#[export]
	#[initial_value(Vector2i::new(10, 10))]
	size: Vector2i,

	#[export(range = (0., 10.))]
	#[initial_value = 2.]
	spacing: real,

	#[export(enum = (Orthographic, Isometric))]
	kind: i32,
}

Connect Godot signals to Bevy events in the Godot editor?

Using the same Godot addon, Godot nodes can ALSO have their signals connected to Bevy events. Just derive from KissingEvent and the event will become available in the Godot editor!

Check out the Events section for more details.

use godot::prelude::*;
use bevy::prelude::*;
use bevy_kissing_godot::prelude::*;

/// A unit Event can connect to any signal.
#[derive(Event, KissingEvent)]
struct OnAnyGodotSignal;

/// Event intended to connect to Button.toggled
#[derive(EntityEvent, KissingEvent)]
struct OnMyButtonPressed {
	// Receive entity for Button node.
	#[event_target]
	entity: Entity,

	// Receive first argument of signal; it must be a bool.
	#[godot_signal_arg(index = 0)]
	toggled_on: bool,
}

Multithreading?

Multithreading is supported as it is in Bevy, but only if you write functions that don’t access NonSend resources. Accessing Godot’s scene tree, all Godot nodes, and all Godot resources requires NonSend, so multithreading is only applicable to behavior and state stored entirely Rust-side.

Setup

Bevy💋Godot is built on top of gdext, so you should start by familiarizing yourself with that project!

Creating a New Project

1. Create New Project

Create a new project as you would with gdext. Here’s a convenient link to its book!

This includes creating a .gdextension file AND creating your own ExtensionLibrary struct.

use godot::prelude::*;

struct PoopPeeExtension;

#[gdextension]
unsafe impl ExtensionLibrary for PoopPeeExtension {
	fn on_stage_init(level: InitStage) {}
	fn on_stage_deinit(level: InitStage) {}
}

2. Install Addon

Download and install the Bevy💋Godot addon from addons/ in the main respository. (Just copy and paste the addons/ folder to the top level of your Godot project; say YES to merge if prompted.)

3. Add Rust Crates

Add Bevy and Bevy💋Godot to your Rust project:

# Become a Bevy enjoyer.
cargo add bevy
# Don't lose sight of Godot.
cargo add bevy_kissing_godot --git https://github.com/SomeRanDev/Bevy-KISSING-Godot

4. Create Bevy App Function

Create a function that takes a mutable reference to Bevy’s App. This will be the entry point for Bevy integration. Add the bevy_kissing_godot::prelude::kiss_bevy attribute with your desired name for your Godot autoload Node.

use bevy::prelude::*;
use bevy_kissing_godot::prelude::*;

#[kiss_bevy(node_name = PoopPeeKisser)]
fn main(app: &mut App) {
	// do stuff with app
}

5. Add Autoload Node

Once you successfully compile your Rust stuff, you need to add your autoload Node (in our case PoopPeeKisser) to the “Autoload” section of your Godot project settings.

First create a GDScript file that extends from your Rust node:

extends PoopPeeKisser

Then you’ll need to:

  1. Go to Project > Project Settings > Globals > Autoload

  2. Set the GDScript file path to “Path”

  3. Give it a name (like PoopPeeKisserGD)

  4. Press the + Add button

6. You’re Now Ready to Go!

Read the title of this step to know if you’re ready to go.

Entry Function (kiss_bevy attribute)

kiss_bevy is the macro attribute used to dictate the entry function.

use bevy::prelude::*;
use bevy_kissing_godot::prelude::*;

#[kiss_bevy(node_name = PoopPeeKisser)]
fn main(app: &mut App) {
	// do stuff with app
}

Declaration and Arguments

In addition to the autoload Node name argument, it has two more optional arguments. The declaration could be seen as something like this (assume Option arguments are optional):

kiss_bevy(
	node_name = Ident,
	process_wrapper = Option<Path>,
	physics_process_wrapper = Option<Path>,
)

process_wrapper and physics_process_wrapper can be assigned paths to macros that take two expression arguments:

  • The first is the original expression that would be generated if the macro wasn’t passed.
  • The second is the self expression for the autoload Node.

process_wrapper wraps the process expression of the generated Bevy app node. physics_process_wrapper does the same, but for the physics_process.

Example

For example, you can configure panic capturing like this:

use bevy::prelude::*;
use bevy_kissing_godot::prelude::*;

/// Wraps the process call with a panic catcher.
macro_rules panic_catcher {
	($original_expression: expr, $self: expr) => {
		let result = std::panic::catch_unwind(|| {
			$original_expression
		});
		if result.is_err() {
			println!("Panic happened!");

			// Check bevy_kissing_godot::kissing_app for all `self.app` functions.
			$self.app.clear_app();
		}
	}
}

#[kiss_bevy(
	node_name = MyAppNodeName,
	process_wrapper = panic_catcher,
	physics_process_wrapper = panic_catcher
)]
fn main(app: &mut App) {
	// Do stuff with `app`...
}

Schedules

Bevy💋Godot’s supported scedules are a little inconsistent, but here’s the breakdown:

Startup

Bevy💋Godot DOES support Startup. It runs immediately after the scene tree is ready, so it basically runs as a _ready function for your initial scene.

use godot::prelude::*;
use bevy::prelude::*;
use bevy_kissing_godot::prelude::*;

#[kiss_bevy(node_name = PoopPeeKisser)]
fn main(app: &mut App) {
	app.add_scedule(Startup, on_game_start)
}

fn on_game_start(scene_tree: NonSend<Gd<SceneTree>>) {
	godot_print!("The game has started with {} nodes!", scene_tree.get_node_count());
}

Update

Update is not supported in Bevy💋Godot. You should use Process instead.

Process

Process runs every “process” frame in Godot. It runs in the _process function in your autoload Node.

You can use the ProcessDelta resource to access the delta value for that process frame.

use godot::prelude::*;
use bevy::prelude::*;
use bevy_kissing_godot::prelude::*;

#[kiss_bevy(node_name = PoopPeeKisser)]
fn main(app: &mut App) {
	app.add_scedule(Process, on_game_update)
}

fn on_game_update(delta: Res<ProcessDelta>) {
	godot_print!("The process frame ran with {} delta.", *delta);
}

PhysicsProcess

PhysicsProcess is the same as Process except for the _physics_process frame.

Similarly, you can use the PhysicsProcessDelta resource to access its delta.

use godot::prelude::*;
use bevy::prelude::*;
use bevy_kissing_godot::prelude::*;

#[kiss_bevy(node_name = PoopPeeKisser)]
fn main(app: &mut App) {
	app.add_scedule(PhysicsProcess, on_game_update_physics)
}

fn on_game_update_physics(delta: Res<PhysicsProcessDelta>) {
	godot_print!("The physics process frame ran with {} delta.", *delta);
}

How it Works

Every Node in Godot will have a Bevy entity generated for it. These Bevy entities can have components attached to them. So, essentially, you attach Bevy components to Godot nodes.

Let’s start by learning how to add components to a Godot node in the editor.

Components

Creating a Bevy component

Let’s start with a normal Bevy component.

use bevy::prelude::*;
use godot::prelude::*;

#[derive(Component)]
struct Health {
	flags: u32,
	position: Vector2,
	current_hp: u32,
	maximum_hp: u32,
}

This is a completely valid and usable component in Bevy💋Godot! Add it to any entity as you like!

Creating a Kissing component

In Bevy💋Godot, a “kissing” component is a component that is exposed and visible in the Godot editor (it crosses the boundary and kisses Godot).

To make a “kissing” component, just add the KissingComponent derive.

use bevy::prelude::*;
use godot::prelude::*;
use bevy_kissing_godot::prelude::*;

#[derive(Component, KissingComponent)]
struct Health {
	flags: u32,
	position: Vector2,
	current_hp: u32,
	maximum_hp: u32,
}

This will appear in the list of components in the Godot editor, but it won’t have any fields to edit!!

Creating a configurable Kissing component

To allow the fields of your kissing component to be editable in Godot, you use the #[export] attribute. This attribute works exactly as it does in gdext (the attribute is passed verbatim to gdext). This means you can use all the gdext parameters for this attribute as well!

use bevy::prelude::*;
use godot::prelude::*;
use bevy_kissing_godot::prelude::*;

#[derive(Component, KissingComponent)]
struct Health {
	#[export(enum = (Segmented = 1, Round = 2, Stacked = 3))] // added
	flags: u32,

	#[export] // added
	position: Vector2,

	#[export(range = (0, 1000))] // added
	maximum_hp: u32,

	current_hp: u32,
}

Setting initial value

To set the initial (and default) value for a property on a kissing component, the #[initial_value] attribute can be used. The expression is passed to an #[init(val = X)] attribute on the component’s editor object.

For literals, the #[initial_value = VALUE] syntax can be used. Rust does not allow all expressions for that attribute syntax, so for more complicated expressions, #[initial_value(VALUE)] must be used.

use bevy::prelude::*;
use godot::prelude::*;
use bevy_kissing_godot::prelude::*;

#[derive(Component, KissingComponent)]
struct Health {
	#[export(enum = (Segmented = 1, Round = 2, Stacked = 3))]
	#[initial_value = 2] // added
	flags: u32,

	#[export]
	#[initial_value(Vector2::new(100., 100.))] // added
	position: Vector2,

	#[export(range = (0, 1000))]
	#[initial_value = 10] // added
	maximum_hp: u32,

	current_hp: u32,
}

Post-constructor

The kissing component is constructed directly from the values provided by the editor. However, you can run a function to modify the component immediately after it’s constructed. Simply assign a function identifier to on_construct as an argument to #[kissing_component].

use bevy::prelude::*;
use godot::prelude::*;
use bevy_kissing_godot::prelude::*;

#[derive(Component, KissingComponent)]
#[kissing_component(on_construct = health_on_construct)] // added
struct Health {
	#[export(enum = (Segmented = 1, Round = 2, Stacked = 3))]
	#[initial_value = 2]
	flags: u32,

	#[export]
	#[initial_value(Vector2::new(100., 100.))]
	position: Vector2,

	#[export(range = (0, 1000))]
	#[initial_value = 10]
	maximum_hp: u32,

	current_hp: u32,
}

// The function just takes a mutable reference to the struct.
// Use Bevy's `on_add` if you want to make queries.
fn health_on_construct(component: &mut Health) {
	// We want the hp to start at the maximum value.
	component.current_hp = component.maximum_hp;
}

Events

Creating a Kissing event

In Bevy💋Godot, a “kissing” event is an event that is exposed and visible in the Godot editor. Nodes can connect their signals to these events.

To make a “kissing” event, just derive from KissingEvent. This works with both Event and EntityEvent.

use bevy::prelude::*;
use bevy_kissing_godot::prelude::*;

/// An event you'd connect to Control.focus_entered or something...
#[derive(Event, KissingEvent)]
struct FocusEntered;

// ---

#[kiss_bevy(node_name = MyGameKisser)]
fn setup(app: &mut App) {
	app.add_observer(on_focus);
}

fn on_focus(event: On<FocusEntered>) {
	// do something...
}

Creating a Kissing entity event

If you want to track which entity/node triggered the signal, you probably want to use an EntityEvent. Note that you MUST use the #[event_target] attribute to mark the entity field:

use bevy::prelude::*;
use bevy_kissing_godot::prelude::*;
use godot::classes::Control;

#[derive(EntityEvent, KissingEvent)]
struct MinimumSizeChanged(#[event_target] Entity);

// or

#[derive(EntityEvent, KissingEvent)]
struct MinimumSizeChanged {
	#[event_target]
	entity: Entity,
}

You can get the node from the entity using a query. Visit Queries for more details.

#[kiss_bevy(node_name = MyGameKisser)]
fn setup(app: &mut App) {
	app.add_observer(on_minimum_size_changed);
}

fn on_minimum_size_changed(
	event: On<MinimumSizeChanged>,
	query: Query<&GodotNodeId, With<GodotNode<Control>>,
	all_nodes: NonSend<AllNodes>,
) {
	let control: &GodotNodeId = query.get(event.entity).unwrap();
	let control: Gd<Control> = control.get_as::<Control>(&all_nodes);
	// do something with control
}

Receiving arguments from signals

Of course, Godot signals also pass data! We can selectively receive data by adding fields to our Kissing event with the #[godot_signal_arg(index = <INDEX>)] attribute. INDEX is the index of the argument we want the field to receive the data from.

For example, the OptionButton.item_selected signal provides a single argument index: int. So we need to add an int-compatible type as a field to our event and annotate it with #[godot_signal_arg(index = 0)].

use bevy::prelude::*;
use bevy_kissing_godot::prelude::*;
use godot::classes::OptionButton;

#[derive(EntityEvent, KissingEvent)]
struct ItemSelected {
	#[event_target]
	entity: Entity,

	#[godot_signal_arg(index = 0)]
	index: i32,
}

// ---

#[kiss_bevy(node_name = MyGameKisser)]
fn setup(app: &mut App) {
	app.add_observer(on_item_selected);
}

fn on_item_selected(
	event: On<ItemSelected>,
	query: Query<&GodotNodeId, With<GodotNode<OptionButton>>,
	all_nodes: NonSend<AllNodes>,
) {
	let option_button_id = query.get(event.entity).unwrap();
	let option_button = option_button_id.get_as::<OptionButton>(&all_nodes);
	godot_print!("Selected option {}", option_button.get_selected());
}

Receiving Gd<T> objects from signals

In some situations, you’ll need to receive a Gd<T> object from a Godot signal. For example, Tree.button_clicked passes a Gd<TreeItem> for the first argument. For this, the gd_handle argument can be added to #[godot_signal_arg] to signify the object needs to be stored as a GdHandle.

use bevy::prelude::*;
use bevy_kissing_godot::prelude::*;
use godot::classes::TreeItem;

#[derive(Event, KissingEvent)]
struct TreeItemButtonClicked {
	#[godot_signal_arg(index = 0, gd_handle)]
	tree_item: GdHandle<TreeItem>,
}

A GdHandle can be converted to a Gd<T> using a NonSend<GodotThreadEnsurer>.

use bevy::prelude::*;
use bevy_kissing_godot::prelude::*;
use godot::classes::TreeItem;

fn on_tree_item_button_clicked(
	event: On<TreeItemButtonClicked>,
	ensurer: NonSend<GodotThreadEnsurer>
) {
	let item: Gd<TreeItem> = event.tree_item.to_gd(&ensurer);
	// do something with `item`...
}

Custom conversion from signal Variant argument

Godot provides the signal’s arguments as Variants when the signal is triggered. Bevy💋Godot then uses the Variant’s to function to convert them to their argument’s type. However, if you’d like to directly control how a Variant argument is converted into the Event field, the from_variant argument can be used.

use bevy::prelude::*;
use bevy_kissing_godot::prelude::*;
use godot::classes::TreeItem;

#[derive(Event, KissingEvent)]
struct TreeItemButtonClicked {
	// Instead of storing the `TreeItem`, store its index
	#[godot_signal_arg(index = 0, from_variant = get_tree_item_index)]
	tree_item_index: i32,
}

// Given the `Variant` representation of a `TreeItem`, get its index
fn get_tree_item_index(v: &Variant) -> i32 {
	let tree_item: Gd<TreeItem> = v.to();
	tree_item.get_index()
}

Extra fields

All fields on the KissingEvent struct must be annotated with #[event_target], #[godot_signal_arg], OR #[godot_signal_value]. #[godot_signal_value] is helpful if you want to have a constant value to fill a field when the event is triggered from the Godot editor.

use bevy::prelude::*;
use bevy_kissing_godot::prelude::*;
use godot::classes::Control;

#[derive(Event, KissingEvent)]
struct UpdateText {
	// If called from Godot, this value will be an empty String
	#[godot_signal_value("".to_string())]
	text: String,
}

fn my_system(mut commands: Commands) {
	// But in your Rust code elsewhere, you can provide a custom value
	commands.trigger(UpdateText {
		text: "my custom text"
	});
}

Manually connecting signals

All KissingEvent-derived structs generate a typed_slot function you can use to connect to Godot signals manually. Its first argument is a &mut SceneTree, so you must connect it using connect_other with the subject being the scene tree.

Its arguments will be the fields of the struct (plus entity if an EntityEvent). So if you had a Gd<OptionButton> (and its Entity), you could manually connect it to the ItemSelected event above by doing:

let option_button: Gd<OptionButton> = /* ... */;
let option_button_entity: Entity = /* ... */;

let scene_tree = option_button.get_tree();
option_button.signals().item_selected.connect_other(&scene_tree, move |index: i32| {
	ItemSelected::typed_slot(option_button_entity, index);
});

If you’d like to connect within a Bevy system function, the NonSend scene tree instance can be used!

use bevy::prelude::*;
use bevy_kissing_godot::prelude::*;
use godot::classes::Control;

fn setup(app: &mut App) {
	app.add_systems(Startup, startup_scene_tree)
		.add_observer(on_close_requested);
}

fn startup_scene_tree(mut scene_tree: NonSendMut<Gd<SceneTree>>) {
	scene_tree.set_auto_accept_quit(false);
	scene_tree
		.get_root()
		.unwrap()
		.signals()
		.close_requested()
		.connect_other(scene_tree.as_ref(), CloseRequested::typed_slot);
}

// ---

#[derive(Event, KissingEvent)]
struct CloseRequested;

fn on_close_requested(_event: On<CloseRequested>) {
	// do something on close...
}

Queries

Okay, so now we have components attached to our Godot through the editor, but how do we query for the Gd<Node>? That’s where we use GodotNodeId!

Accessing Gd<T> Node Instance

Every Bevy entity that correlates to a Godot node will have a GodotNodeId component. This is just a wrapper for a simple u32 that is the unique Bevy💋Godot ID for the node. We can convert this ID into a Gd<T> reference using the non-send AllNodes resource.

use godot::prelude::*;
use bevy::prelude::*;
use bevy_kissing_godot::prelude::*;

#[kiss_bevy(node_name = PoopPeeKisser)]
fn main(app: &mut App) {
	app.add_scedule(Process, update_health);
}

#[derive(Component)]
struct Health {
	value: real,
}

fn update_health(
	query: Query<(&mut Health, &GodotNodeId)>,
	delta: Res<ProcessDelta>,
	all_nodes: NonSend<AllNodes>,
) {
	for (health, godot_node_id) in query {
		let node_3d = godot_node_id::try_get_as<Node3D>(&all_nodes).unwrap();

		// Decrement health whenever the y position is below zero.
		if node_3d.get_position().y < 0. {
			health.value -= delta.as_real();
		}
	}
}

Filtering Queries by Node Class

We can filter the query by the node’s class by using GodotNode! The GodotNode struct takes a generic argument that you can use to specify the Godot class type.

Note this filter will include all child classes. So a Node3D filter will include MeshInstance3Ds, etc.

use godot::prelude::*;
use bevy::prelude::*;
use bevy_kissing_godot::prelude::*;

#[kiss_bevy(node_name = PoopPeeKisser)]
fn main(app: &mut App) {
	app.add_scedule(Process, update_float);
}

#[derive(Component)]
struct Floaty {
	vertical_speed: real,
}

fn update_float(
	query: Query<
		(&Floaty, &GodotNodeId),
		With<GodotNode<CharacterBody3D>> // only CharacterBody3Ds
	>,
	delta: Res<ProcessDelta>,
	all_nodes: NonSend<AllNodes>,
) {
	for (floaty, godot_node_id) in query {
		// This unwrap is guarenteed to succeed cause they're ALL CharacterBody3D.
		let mut character =
			godot_node_id::try_get_as<CharacterBody3D>(&all_nodes).unwrap();

		// Move up a little
		let speed = delta.as_real() * floaty.vertical_speed;
		let old_position = character.get_position();
		character.set_position(old_position + Vector3::UP * speed);
	}
}

Make Your Nodes Queryable

You can still make custom Node classes as you would in gdext, BUT your custom nodes will not be queryable unless you add the KissingNode derive.

use godot::prelude::*;
use godot::classes::Sprite2D;
use bevy_kissing_godot::prelude::*;

#[derive(GodotClass, KissingNode)]
#[class(init, base=Sprite2D)]
struct Player {
    base: Base<Sprite2D>,
}

This will now work:

use godot::prelude::*;
use bevy::prelude::*;
use bevy_kissing_godot::prelude::*;

#[kiss_bevy(node_name = PoopPeeKisser)]
fn main(app: &mut App) {
	app.add_scedule(Process, update_float);
}

#[derive(Component)]
struct Floaty {
	vertical_speed: real,
}

fn update_float(
	player: Single<
		(&Floaty, &GodotNodeId),
		With<GodotNode<Player>> // only Player
	>,
	delta: Res<ProcessDelta>,
	all_nodes: NonSend<AllNodes>,
) {
	let (floaty, player_id) = player.into_inner();
	// do stuff
}

On Bevy Entity Ready

BevyEntityReady is a trait you can implement on a custom gdext Node to run a function immediately after its Bevy entity is created. You can add components and do other things to it.

use godot::prelude::*;
use godot::classes::Sprite2D;
use bevy::prelude::*;
use bevy_kissing_godot::prelude::*;

#[derive(GodotClass)]
#[class(init, base=Sprite2D)]
struct Player {
    base: Base<Sprite2D>,

    #[export(range = (0, 100))]
    initial_hp: u32,
}

#[derive(Component)]
struct Health {
	hp: u32,
}

// MAKE SURE YOU ADD #[godot_dyn]
#[godot_dyn]
impl BevyEntityReady for Player {
	fn bevy_entity_ready<'a>(&mut self, entity: EntityWorldMut<'a>) {
		// It'd probably be better to do this as a kissing component,
		// but this is just an example of how it works so shut up.
		entity.insert(Health {
			hp: self.initial_hp
		});
	}
}