diff --git a/doc/classes/PhysicsServer2D.xml b/doc/classes/PhysicsServer2D.xml
index b16b1551fa24..d7bd46590635 100644
--- a/doc/classes/PhysicsServer2D.xml
+++ b/doc/classes/PhysicsServer2D.xml
@@ -951,6 +951,14 @@
Creates a 2D space in the physics server, and returns the [RID] that identifies it. A space contains bodies and areas, and controls the stepping of the physics simulation of the objects in it.
+
+
+
+
+ Runs the contact and area monitoring callbacks queued by [param space], so that the results of a manual [method space_step] become observable.
+ [b]Note:[/b] [param space] must not be active. The engine already flushes queries for active spaces every physics frame.
+
+
@@ -958,6 +966,14 @@
Returns the state of a space, a [PhysicsDirectSpaceState2D]. This object can be used for collision/intersection queries.
+
+
+
+
+
+ Returns information about the current state of [param space]. See [enum ProcessInfo] for a list of available states.
+
+
@@ -990,6 +1006,16 @@
Sets the value of the given space parameter.
+
+
+
+
+
+ Manually advances [param space] forward by [param delta] seconds. This can be used to fast-forward a simulation, as in rollback-style networking, or to predict an outcome, such as previewing the path of a ball in a billiards game.
+ [b]Note:[/b] [param space] must not be active. An active space is already advanced once per physics frame by the engine, so stepping it manually would advance it twice. Deactivate it with [method space_set_active] first.
+ [b]Note:[/b] Stepping does not run contact or area monitoring callbacks. Call [method space_flush_queries] afterwards if you need them.
+
+
diff --git a/doc/classes/PhysicsServer2DExtension.xml b/doc/classes/PhysicsServer2DExtension.xml
index 3299f3ac586b..9aef1d12be66 100644
--- a/doc/classes/PhysicsServer2DExtension.xml
+++ b/doc/classes/PhysicsServer2DExtension.xml
@@ -1025,6 +1025,12 @@
Overridable version of [method PhysicsServer2D.space_create].
+
+
+
+
+
+
@@ -1048,6 +1054,13 @@
Overridable version of [method PhysicsServer2D.space_get_direct_state].
+
+
+
+
+
+
+
@@ -1089,6 +1102,13 @@
Overridable version of [method PhysicsServer2D.space_set_param].
+
+
+
+
+
+
+
diff --git a/doc/classes/PhysicsServer3D.xml b/doc/classes/PhysicsServer3D.xml
index a06a88986d0d..dc2bc6b4e1fa 100644
--- a/doc/classes/PhysicsServer3D.xml
+++ b/doc/classes/PhysicsServer3D.xml
@@ -1404,6 +1404,14 @@
Creates a space. A space is a collection of parameters for the physics engine that can be assigned to an area or a body. It can be assigned to an area with [method area_set_space], or to a body with [method body_set_space].
+
+
+
+
+ Runs the contact and area monitoring callbacks queued by [param space], so that the results of a manual [method space_step] become observable.
+ [b]Note:[/b] [param space] must not be active. The engine already flushes queries for active spaces every physics frame.
+
+
@@ -1411,6 +1419,14 @@
Returns the state of a space, a [PhysicsDirectSpaceState3D]. This object can be used to make collision/intersection queries.
+
+
+
+
+
+ Returns information about the current state of [param space]. See [enum ProcessInfo] for a list of available states.
+
+
@@ -1443,6 +1459,16 @@
Sets the value for a space parameter. A list of available parameters is on the [enum SpaceParameter] constants.
+
+
+
+
+
+ Manually advances [param space] forward by [param delta] seconds. This can be used to fast-forward a simulation, as in rollback-style networking, or to predict an outcome, such as previewing the path of a ball in a billiards game.
+ [b]Note:[/b] [param space] must not be active. An active space is already advanced once per physics frame by the engine, so stepping it manually would advance it twice. Deactivate it with [method space_set_active] first.
+ [b]Note:[/b] Stepping does not run contact or area monitoring callbacks. Call [method space_flush_queries] afterwards if you need them.
+
+
diff --git a/doc/classes/PhysicsServer3DExtension.xml b/doc/classes/PhysicsServer3DExtension.xml
index e5c2e59864d3..be8ed801b19b 100644
--- a/doc/classes/PhysicsServer3DExtension.xml
+++ b/doc/classes/PhysicsServer3DExtension.xml
@@ -1270,6 +1270,12 @@
+
+
+
+
+
+
@@ -1288,6 +1294,13 @@
+
+
+
+
+
+
+
@@ -1323,6 +1336,13 @@
+
+
+
+
+
+
+
diff --git a/modules/godot_physics_2d/godot_physics_server_2d.cpp b/modules/godot_physics_2d/godot_physics_server_2d.cpp
index 9054f4746d34..5a66e74a7dbf 100644
--- a/modules/godot_physics_2d/godot_physics_server_2d.cpp
+++ b/modules/godot_physics_2d/godot_physics_server_2d.cpp
@@ -244,6 +244,48 @@ bool GodotPhysicsServer2D::space_is_active(RID p_space) const {
return active_spaces.has(space);
}
+void GodotPhysicsServer2D::space_step(RID p_space, real_t p_delta) {
+ GodotSpace2D *space = space_owner.get_or_null(p_space);
+ ERR_FAIL_NULL(space);
+ ERR_FAIL_COND_MSG(active_spaces.has(space), "An active space can't be stepped manually. Deactivate it with space_set_active() first.");
+
+ // Only drain the pending shape updates that belong to this space, rather than calling
+ // _update_shapes(), so that stepping one space never mutates another. Manually stepped
+ // spaces are typically used for lockstep/rollback simulation, where that isolation matters.
+ SelfList *collision_object_self = pending_shape_update_list.first();
+ while (collision_object_self) {
+ if (collision_object_self->self()->get_space() == space) {
+ collision_object_self->self()->_shape_changed();
+
+ SelfList *to_remove = collision_object_self;
+ collision_object_self = collision_object_self->next();
+
+ pending_shape_update_list.remove(to_remove);
+ } else {
+ collision_object_self = collision_object_self->next();
+ }
+ }
+
+ stepper->step(space, p_delta);
+}
+
+void GodotPhysicsServer2D::space_flush_queries(RID p_space) {
+ GodotSpace2D *space = space_owner.get_or_null(p_space);
+ ERR_FAIL_NULL(space);
+ ERR_FAIL_COND_MSG(active_spaces.has(space), "An active space should not flush queries manually. Deactivate it with space_set_active() first.");
+
+ // Validate before raising the flag: an early return with it still set would leave the
+ // server permanently "flushing", silently disabling the guards that read
+ // is_flushing_queries() (e.g. Area2D::set_monitorable). Save and restore rather than
+ // forcing false, so a nested flush from inside a physics callback can't clear the outer one.
+ const bool was_flushing_queries = flushing_queries;
+ flushing_queries = true;
+
+ space->call_queries();
+
+ flushing_queries = was_flushing_queries;
+}
+
void GodotPhysicsServer2D::space_set_param(RID p_space, PS2DE::SpaceParameter p_param, real_t p_value) {
GodotSpace2D *space = space_owner.get_or_null(p_space);
ERR_FAIL_NULL(space);
@@ -1388,6 +1430,25 @@ int GodotPhysicsServer2D::get_process_info(PS2DE::ProcessInfo p_info) {
return 0;
}
+int GodotPhysicsServer2D::space_get_last_process_info(RID p_space, PS2DE::ProcessInfo p_info) {
+ GodotSpace2D *space = space_owner.get_or_null(p_space);
+ ERR_FAIL_NULL_V(space, 0);
+
+ switch (p_info) {
+ case PS2DE::INFO_ACTIVE_OBJECTS: {
+ return space->get_active_objects();
+ } break;
+ case PS2DE::INFO_COLLISION_PAIRS: {
+ return space->get_collision_pairs();
+ } break;
+ case PS2DE::INFO_ISLAND_COUNT: {
+ return space->get_island_count();
+ } break;
+ }
+
+ return 0;
+}
+
GodotPhysicsServer2D *GodotPhysicsServer2D::godot_singleton = nullptr;
GodotPhysicsServer2D::GodotPhysicsServer2D(bool p_using_threads) {
diff --git a/modules/godot_physics_2d/godot_physics_server_2d.h b/modules/godot_physics_2d/godot_physics_server_2d.h
index 0e9b7979a174..59b2a197afd4 100644
--- a/modules/godot_physics_2d/godot_physics_server_2d.h
+++ b/modules/godot_physics_2d/godot_physics_server_2d.h
@@ -107,6 +107,8 @@ class GodotPhysicsServer2D : public PhysicsServer2D {
virtual RID space_create() override;
virtual void space_set_active(RID p_space, bool p_active) override;
virtual bool space_is_active(RID p_space) const override;
+ virtual void space_step(RID p_space, real_t p_delta) override;
+ virtual void space_flush_queries(RID p_space) override;
virtual void space_set_param(RID p_space, PS2DE::SpaceParameter p_param, real_t p_value) override;
virtual real_t space_get_param(RID p_space, PS2DE::SpaceParameter p_param) const override;
@@ -298,6 +300,7 @@ class GodotPhysicsServer2D : public PhysicsServer2D {
virtual bool is_flushing_queries() const override { return flushing_queries; }
int get_process_info(PS2DE::ProcessInfo p_info) override;
+ int space_get_last_process_info(RID p_space, PS2DE::ProcessInfo p_info) override;
GodotPhysicsServer2D(bool p_using_threads = false);
~GodotPhysicsServer2D() {}
diff --git a/modules/godot_physics_3d/godot_physics_server_3d.cpp b/modules/godot_physics_3d/godot_physics_server_3d.cpp
index d02ce62b651b..0181bee4a385 100644
--- a/modules/godot_physics_3d/godot_physics_server_3d.cpp
+++ b/modules/godot_physics_3d/godot_physics_server_3d.cpp
@@ -175,6 +175,48 @@ bool GodotPhysicsServer3D::space_is_active(RID p_space) const {
return active_spaces.has(space);
}
+void GodotPhysicsServer3D::space_step(RID p_space, real_t p_delta) {
+ GodotSpace3D *space = space_owner.get_or_null(p_space);
+ ERR_FAIL_NULL(space);
+ ERR_FAIL_COND_MSG(active_spaces.has(space), "An active space can't be stepped manually. Deactivate it with space_set_active() first.");
+
+ // Only drain the pending shape updates that belong to this space, rather than calling
+ // _update_shapes(), so that stepping one space never mutates another. Manually stepped
+ // spaces are typically used for lockstep/rollback simulation, where that isolation matters.
+ SelfList *collision_object_self = pending_shape_update_list.first();
+ while (collision_object_self) {
+ if (collision_object_self->self()->get_space() == space) {
+ collision_object_self->self()->_shape_changed();
+
+ SelfList *to_remove = collision_object_self;
+ collision_object_self = collision_object_self->next();
+
+ pending_shape_update_list.remove(to_remove);
+ } else {
+ collision_object_self = collision_object_self->next();
+ }
+ }
+
+ stepper->step(space, p_delta);
+}
+
+void GodotPhysicsServer3D::space_flush_queries(RID p_space) {
+ GodotSpace3D *space = space_owner.get_or_null(p_space);
+ ERR_FAIL_NULL(space);
+ ERR_FAIL_COND_MSG(active_spaces.has(space), "An active space should not flush queries manually. Deactivate it with space_set_active() first.");
+
+ // Validate before raising the flag: an early return with it still set would leave the
+ // server permanently "flushing", silently disabling the guards that read
+ // is_flushing_queries() (e.g. Area3D::set_monitorable). Save and restore rather than
+ // forcing false, so a nested flush from inside a physics callback can't clear the outer one.
+ const bool was_flushing_queries = flushing_queries;
+ flushing_queries = true;
+
+ space->call_queries();
+
+ flushing_queries = was_flushing_queries;
+}
+
void GodotPhysicsServer3D::space_set_param(RID p_space, PS3DE::SpaceParameter p_param, real_t p_value) {
GodotSpace3D *space = space_owner.get_or_null(p_space);
ERR_FAIL_NULL(space);
@@ -1786,6 +1828,25 @@ int GodotPhysicsServer3D::get_process_info(PS3DE::ProcessInfo p_info) {
return 0;
}
+int GodotPhysicsServer3D::space_get_last_process_info(RID p_space, PS3DE::ProcessInfo p_info) {
+ GodotSpace3D *space = space_owner.get_or_null(p_space);
+ ERR_FAIL_NULL_V(space, 0);
+
+ switch (p_info) {
+ case PS3DE::INFO_ACTIVE_OBJECTS: {
+ return space->get_active_objects();
+ } break;
+ case PS3DE::INFO_COLLISION_PAIRS: {
+ return space->get_collision_pairs();
+ } break;
+ case PS3DE::INFO_ISLAND_COUNT: {
+ return space->get_island_count();
+ } break;
+ }
+
+ return 0;
+}
+
void GodotPhysicsServer3D::_update_shapes() {
while (pending_shape_update_list.first()) {
pending_shape_update_list.first()->self()->_shape_changed();
diff --git a/modules/godot_physics_3d/godot_physics_server_3d.h b/modules/godot_physics_3d/godot_physics_server_3d.h
index 58410fb1f078..bce4863447ff 100644
--- a/modules/godot_physics_3d/godot_physics_server_3d.h
+++ b/modules/godot_physics_3d/godot_physics_server_3d.h
@@ -105,6 +105,8 @@ class GodotPhysicsServer3D : public PhysicsServer3D {
virtual RID space_create() override;
virtual void space_set_active(RID p_space, bool p_active) override;
virtual bool space_is_active(RID p_space) const override;
+ virtual void space_step(RID p_space, real_t p_delta) override;
+ virtual void space_flush_queries(RID p_space) override;
virtual void space_set_param(RID p_space, PS3DE::SpaceParameter p_param, real_t p_value) override;
virtual real_t space_get_param(RID p_space, PS3DE::SpaceParameter p_param) const override;
@@ -387,6 +389,7 @@ class GodotPhysicsServer3D : public PhysicsServer3D {
virtual bool is_flushing_queries() const override { return flushing_queries; }
int get_process_info(PS3DE::ProcessInfo p_info) override;
+ int space_get_last_process_info(RID p_space, PS3DE::ProcessInfo p_info) override;
GodotPhysicsServer3D(bool p_using_threads = false);
~GodotPhysicsServer3D() {}
diff --git a/modules/jolt_physics/jolt_physics_server_3d.cpp b/modules/jolt_physics/jolt_physics_server_3d.cpp
index af097beb7733..4683badb8589 100644
--- a/modules/jolt_physics/jolt_physics_server_3d.cpp
+++ b/modules/jolt_physics/jolt_physics_server_3d.cpp
@@ -214,6 +214,38 @@ bool JoltPhysicsServer3D::space_is_active(RID p_space) const {
return active_spaces.has(space);
}
+void JoltPhysicsServer3D::space_step(RID p_space, real_t p_delta) {
+ JoltSpace3D *space = space_owner.get_or_null(p_space);
+ ERR_FAIL_NULL(space);
+ ERR_FAIL_COND(space->is_stepping());
+ // Match GodotPhysics: an active space is already advanced by step(), so stepping it
+ // again here would double-advance the simulation.
+ ERR_FAIL_COND_MSG(active_spaces.has(space), "An active space can't be stepped manually. Deactivate it with space_set_active() first.");
+
+ // Bracket with the job system exactly as step() does, otherwise repeated manual steps
+ // exhaust Jolt's job allocator ("maximum number of jobs exceeded").
+ job_system->pre_step();
+
+ space->step((float)p_delta);
+
+ job_system->post_step();
+}
+
+void JoltPhysicsServer3D::space_flush_queries(RID p_space) {
+ JoltSpace3D *space = space_owner.get_or_null(p_space);
+ ERR_FAIL_NULL(space);
+ ERR_FAIL_COND_MSG(active_spaces.has(space), "An active space should not flush queries manually. Deactivate it with space_set_active() first.");
+
+ // Validate before raising the flag, and restore rather than force false, so an early
+ // return or a nested flush cannot leave the server stuck in the flushing state.
+ const bool was_flushing_queries = flushing_queries;
+ flushing_queries = true;
+
+ space->call_queries();
+
+ flushing_queries = was_flushing_queries;
+}
+
void JoltPhysicsServer3D::space_set_param(RID p_space, PS3DE::SpaceParameter p_param, real_t p_value) {
JoltSpace3D *space = space_owner.get_or_null(p_space);
ERR_FAIL_NULL(space);
@@ -1688,6 +1720,10 @@ int JoltPhysicsServer3D::get_process_info(PS3DE::ProcessInfo p_process_info) {
return 0;
}
+int JoltPhysicsServer3D::space_get_last_process_info(RID p_space, PS3DE::ProcessInfo p_info) {
+ return 0;
+}
+
void JoltPhysicsServer3D::free_space(JoltSpace3D *p_space) {
ERR_FAIL_NULL(p_space);
diff --git a/modules/jolt_physics/jolt_physics_server_3d.h b/modules/jolt_physics/jolt_physics_server_3d.h
index 244de204d4dd..439d61492dff 100644
--- a/modules/jolt_physics/jolt_physics_server_3d.h
+++ b/modules/jolt_physics/jolt_physics_server_3d.h
@@ -152,6 +152,8 @@ class JoltPhysicsServer3D final : public PhysicsServer3D {
virtual void space_set_active(RID p_space, bool p_active) override;
virtual bool space_is_active(RID p_space) const override;
+ virtual void space_step(RID p_space, real_t p_delta) override;
+ virtual void space_flush_queries(RID p_space) override;
virtual void space_set_param(RID p_space, PS3DE::SpaceParameter p_param, real_t p_value) override;
virtual real_t space_get_param(RID p_space, PS3DE::SpaceParameter p_param) const override;
@@ -430,6 +432,7 @@ class JoltPhysicsServer3D final : public PhysicsServer3D {
virtual bool is_flushing_queries() const override;
virtual int get_process_info(PS3DE::ProcessInfo p_process_info) override;
+ virtual int space_get_last_process_info(RID p_space, PS3DE::ProcessInfo p_info) override;
bool is_on_separate_thread() const { return on_separate_thread; }
bool is_active() const { return active; }
diff --git a/servers/physics_2d/physics_server_2d.cpp b/servers/physics_2d/physics_server_2d.cpp
index d414c76865c7..9545cc3fa92b 100644
--- a/servers/physics_2d/physics_server_2d.cpp
+++ b/servers/physics_2d/physics_server_2d.cpp
@@ -69,6 +69,8 @@ void PhysicsServer2D::_bind_methods() {
ClassDB::bind_method(D_METHOD("space_create"), &PhysicsServer2D::space_create);
ClassDB::bind_method(D_METHOD("space_set_active", "space", "active"), &PhysicsServer2D::space_set_active);
ClassDB::bind_method(D_METHOD("space_is_active", "space"), &PhysicsServer2D::space_is_active);
+ ClassDB::bind_method(D_METHOD("space_step", "space", "delta"), &PhysicsServer2D::space_step);
+ ClassDB::bind_method(D_METHOD("space_flush_queries", "space"), &PhysicsServer2D::space_flush_queries);
ClassDB::bind_method(D_METHOD("space_set_param", "space", "param", "value"), &PhysicsServer2D::space_set_param);
ClassDB::bind_method(D_METHOD("space_get_param", "space", "param"), &PhysicsServer2D::space_get_param);
ClassDB::bind_method(D_METHOD("space_get_direct_state", "space"), &PhysicsServer2D::space_get_direct_state);
@@ -228,6 +230,7 @@ void PhysicsServer2D::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_active", "active"), &PhysicsServer2D::set_active);
ClassDB::bind_method(D_METHOD("get_process_info", "process_info"), &PhysicsServer2D::get_process_info);
+ ClassDB::bind_method(D_METHOD("space_get_last_process_info", "space", "process_info"), &PhysicsServer2D::space_get_last_process_info);
BIND_ENUM_CONSTANT(PS2DE::SPACE_PARAM_CONTACT_RECYCLE_RADIUS);
BIND_ENUM_CONSTANT(PS2DE::SPACE_PARAM_CONTACT_MAX_SEPARATION);
diff --git a/servers/physics_2d/physics_server_2d.h b/servers/physics_2d/physics_server_2d.h
index d5d9bff49e3c..92a3c1504102 100644
--- a/servers/physics_2d/physics_server_2d.h
+++ b/servers/physics_2d/physics_server_2d.h
@@ -81,6 +81,8 @@ class PhysicsServer2D : public Object {
virtual RID space_create() = 0;
virtual void space_set_active(RID p_space, bool p_active) = 0;
virtual bool space_is_active(RID p_space) const = 0;
+ virtual void space_step(RID p_space, real_t p_delta) = 0;
+ virtual void space_flush_queries(RID p_space) = 0;
virtual void space_set_param(RID p_space, PS2DE::SpaceParameter p_param, real_t p_value) = 0;
virtual real_t space_get_param(RID p_space, PS2DE::SpaceParameter p_param) const = 0;
@@ -288,6 +290,7 @@ class PhysicsServer2D : public Object {
virtual bool is_flushing_queries() const = 0;
virtual int get_process_info(PS2DE::ProcessInfo p_info) = 0;
+ virtual int space_get_last_process_info(RID p_space, PS2DE::ProcessInfo p_info) = 0;
PhysicsServer2D();
~PhysicsServer2D();
diff --git a/servers/physics_2d/physics_server_2d_dummy.h b/servers/physics_2d/physics_server_2d_dummy.h
index f7739e5246f1..3ee6e839cbf1 100644
--- a/servers/physics_2d/physics_server_2d_dummy.h
+++ b/servers/physics_2d/physics_server_2d_dummy.h
@@ -156,6 +156,8 @@ class PhysicsServer2DDummy : public PhysicsServer2D {
virtual RID space_create() override { return RID(); }
virtual void space_set_active(RID p_space, bool p_active) override {}
virtual bool space_is_active(RID p_space) const override { return false; }
+ virtual void space_step(RID p_space, real_t p_delta) override {}
+ virtual void space_flush_queries(RID p_space) override {}
virtual void space_set_param(RID p_space, PS2DE::SpaceParameter p_param, real_t p_value) override {}
virtual real_t space_get_param(RID p_space, PS2DE::SpaceParameter p_param) const override { return 0; }
@@ -352,4 +354,5 @@ class PhysicsServer2DDummy : public PhysicsServer2D {
virtual bool is_flushing_queries() const override { return false; }
virtual int get_process_info(PS2DE::ProcessInfo p_info) override { return 0; }
+ virtual int space_get_last_process_info(RID p_space, PS2DE::ProcessInfo p_info) override { return 0; }
};
diff --git a/servers/physics_2d/physics_server_2d_extension.cpp b/servers/physics_2d/physics_server_2d_extension.cpp
index a7292cb54256..fb2aabeb1392 100644
--- a/servers/physics_2d/physics_server_2d_extension.cpp
+++ b/servers/physics_2d/physics_server_2d_extension.cpp
@@ -159,6 +159,8 @@ void PhysicsServer2DExtension::_bind_methods() {
GDVIRTUAL_BIND(_space_create);
GDVIRTUAL_BIND(_space_set_active, "space", "active");
GDVIRTUAL_BIND(_space_is_active, "space");
+ GDVIRTUAL_BIND(_space_step, "space", "delta");
+ GDVIRTUAL_BIND(_space_flush_queries, "space");
GDVIRTUAL_BIND(_space_set_param, "space", "param", "value");
GDVIRTUAL_BIND(_space_get_param, "space", "param");
@@ -350,6 +352,7 @@ void PhysicsServer2DExtension::_bind_methods() {
GDVIRTUAL_BIND(_is_flushing_queries);
GDVIRTUAL_BIND(_get_process_info, "process_info");
+ GDVIRTUAL_BIND(_space_get_last_process_info, "space", "process_info");
#ifndef DISABLE_DEPRECATED
GDVIRTUAL_BIND_COMPAT(_body_set_shape_as_one_way_collision_bind_compat_104736, "body", "shape_idx", "enable", "margin");
diff --git a/servers/physics_2d/physics_server_2d_extension.h b/servers/physics_2d/physics_server_2d_extension.h
index 23f435a88ee1..ba7d8da2c5d3 100644
--- a/servers/physics_2d/physics_server_2d_extension.h
+++ b/servers/physics_2d/physics_server_2d_extension.h
@@ -235,6 +235,8 @@ class PhysicsServer2DExtension : public PhysicsServer2D {
EXBIND0R(RID, space_create)
EXBIND2(space_set_active, RID, bool)
EXBIND1RC(bool, space_is_active, RID)
+ EXBIND2(space_step, RID, real_t)
+ EXBIND1(space_flush_queries, RID)
EXBIND3(space_set_param, RID, PS2DE::SpaceParameter, real_t)
EXBIND2RC(real_t, space_get_param, RID, PS2DE::SpaceParameter)
@@ -456,6 +458,7 @@ class PhysicsServer2DExtension : public PhysicsServer2D {
EXBIND0RC(bool, is_flushing_queries)
EXBIND1R(int, get_process_info, PS2DE::ProcessInfo)
+ EXBIND2R(int, space_get_last_process_info, RID, PS2DE::ProcessInfo)
PhysicsServer2DExtension();
~PhysicsServer2DExtension();
diff --git a/servers/physics_2d/physics_server_2d_wrap_mt.h b/servers/physics_2d/physics_server_2d_wrap_mt.h
index dd6afb9333c6..f850b43d0271 100644
--- a/servers/physics_2d/physics_server_2d_wrap_mt.h
+++ b/servers/physics_2d/physics_server_2d_wrap_mt.h
@@ -107,6 +107,8 @@ class PhysicsServer2DWrapMT : public PhysicsServer2D {
FUNCRID(space);
FUNC2(space_set_active, RID, bool);
FUNC1RC(bool, space_is_active, RID);
+ FUNC2(space_step, RID, real_t);
+ FUNC1(space_flush_queries, RID);
FUNC3(space_set_param, RID, PS2DE::SpaceParameter, real_t);
FUNC2RC(real_t, space_get_param, RID, PS2DE::SpaceParameter);
@@ -328,6 +330,10 @@ class PhysicsServer2DWrapMT : public PhysicsServer2D {
return physics_server_2d->get_process_info(p_info);
}
+ int space_get_last_process_info(RID p_space, PS2DE::ProcessInfo p_info) override {
+ return physics_server_2d->space_get_last_process_info(p_space, p_info);
+ }
+
PhysicsServer2DWrapMT(PhysicsServer2D *p_contained, bool p_create_thread);
~PhysicsServer2DWrapMT();
diff --git a/servers/physics_3d/physics_server_3d.cpp b/servers/physics_3d/physics_server_3d.cpp
index bba8ec29afc0..e630dfdf780e 100644
--- a/servers/physics_3d/physics_server_3d.cpp
+++ b/servers/physics_3d/physics_server_3d.cpp
@@ -102,6 +102,8 @@ void PhysicsServer3D::_bind_methods() {
ClassDB::bind_method(D_METHOD("space_create"), &PhysicsServer3D::space_create);
ClassDB::bind_method(D_METHOD("space_set_active", "space", "active"), &PhysicsServer3D::space_set_active);
ClassDB::bind_method(D_METHOD("space_is_active", "space"), &PhysicsServer3D::space_is_active);
+ ClassDB::bind_method(D_METHOD("space_step", "space", "delta"), &PhysicsServer3D::space_step);
+ ClassDB::bind_method(D_METHOD("space_flush_queries", "space"), &PhysicsServer3D::space_flush_queries);
ClassDB::bind_method(D_METHOD("space_set_param", "space", "param", "value"), &PhysicsServer3D::space_set_param);
ClassDB::bind_method(D_METHOD("space_get_param", "space", "param"), &PhysicsServer3D::space_get_param);
ClassDB::bind_method(D_METHOD("space_get_direct_state", "space"), &PhysicsServer3D::space_get_direct_state);
@@ -440,6 +442,7 @@ void PhysicsServer3D::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_active", "active"), &PhysicsServer3D::set_active);
ClassDB::bind_method(D_METHOD("get_process_info", "process_info"), &PhysicsServer3D::get_process_info);
+ ClassDB::bind_method(D_METHOD("space_get_last_process_info", "space", "process_info"), &PhysicsServer3D::space_get_last_process_info);
BIND_ENUM_CONSTANT(PS3DE::SHAPE_WORLD_BOUNDARY);
BIND_ENUM_CONSTANT(PS3DE::SHAPE_SEPARATION_RAY);
diff --git a/servers/physics_3d/physics_server_3d.h b/servers/physics_3d/physics_server_3d.h
index 8acbe395c706..0dab396abb19 100644
--- a/servers/physics_3d/physics_server_3d.h
+++ b/servers/physics_3d/physics_server_3d.h
@@ -90,6 +90,8 @@ class PhysicsServer3D : public Object {
virtual RID space_create() = 0;
virtual void space_set_active(RID p_space, bool p_active) = 0;
virtual bool space_is_active(RID p_space) const = 0;
+ virtual void space_step(RID p_space, real_t p_delta) = 0;
+ virtual void space_flush_queries(RID p_space) = 0;
virtual void space_set_param(RID p_space, PS3DE::SpaceParameter p_param, real_t p_value) = 0;
virtual real_t space_get_param(RID p_space, PS3DE::SpaceParameter p_param) const = 0;
@@ -390,6 +392,7 @@ class PhysicsServer3D : public Object {
virtual bool is_flushing_queries() const = 0;
virtual int get_process_info(PS3DE::ProcessInfo p_info) = 0;
+ virtual int space_get_last_process_info(RID p_space, PS3DE::ProcessInfo p_info) = 0;
PhysicsServer3D();
~PhysicsServer3D();
diff --git a/servers/physics_3d/physics_server_3d_dummy.h b/servers/physics_3d/physics_server_3d_dummy.h
index 6e120655fcdc..32411daf050c 100644
--- a/servers/physics_3d/physics_server_3d_dummy.h
+++ b/servers/physics_3d/physics_server_3d_dummy.h
@@ -162,6 +162,8 @@ class PhysicsServer3DDummy : public PhysicsServer3D {
virtual RID space_create() override { return RID(); }
virtual void space_set_active(RID p_space, bool p_active) override {}
virtual bool space_is_active(RID p_space) const override { return false; }
+ virtual void space_step(RID p_space, real_t p_delta) override {}
+ virtual void space_flush_queries(RID p_space) override {}
virtual void space_set_param(RID p_space, PS3DE::SpaceParameter p_param, real_t p_value) override {}
virtual real_t space_get_param(RID p_space, PS3DE::SpaceParameter p_param) const override { return 0; }
@@ -447,4 +449,5 @@ class PhysicsServer3DDummy : public PhysicsServer3D {
virtual bool is_flushing_queries() const override { return false; }
virtual int get_process_info(PS3DE::ProcessInfo p_info) override { return 0; }
+ virtual int space_get_last_process_info(RID p_space, PS3DE::ProcessInfo p_info) override { return 0; }
};
diff --git a/servers/physics_3d/physics_server_3d_extension.cpp b/servers/physics_3d/physics_server_3d_extension.cpp
index 6d5e9f4eded0..170f8d86da3b 100644
--- a/servers/physics_3d/physics_server_3d_extension.cpp
+++ b/servers/physics_3d/physics_server_3d_extension.cpp
@@ -165,6 +165,8 @@ void PhysicsServer3DExtension::_bind_methods() {
GDVIRTUAL_BIND(_space_create);
GDVIRTUAL_BIND(_space_set_active, "space", "active");
GDVIRTUAL_BIND(_space_is_active, "space");
+ GDVIRTUAL_BIND(_space_step, "space", "delta");
+ GDVIRTUAL_BIND(_space_flush_queries, "space");
GDVIRTUAL_BIND(_space_set_param, "space", "param", "value");
GDVIRTUAL_BIND(_space_get_param, "space", "param");
@@ -441,6 +443,7 @@ void PhysicsServer3DExtension::_bind_methods() {
GDVIRTUAL_BIND(_is_flushing_queries);
GDVIRTUAL_BIND(_get_process_info, "process_info");
+ GDVIRTUAL_BIND(_space_get_last_process_info, "space", "process_info");
}
PhysicsServer3DExtension::PhysicsServer3DExtension() {
diff --git a/servers/physics_3d/physics_server_3d_extension.h b/servers/physics_3d/physics_server_3d_extension.h
index 8e3ab50e0ffc..bac65d34b62a 100644
--- a/servers/physics_3d/physics_server_3d_extension.h
+++ b/servers/physics_3d/physics_server_3d_extension.h
@@ -237,6 +237,8 @@ class PhysicsServer3DExtension : public PhysicsServer3D {
EXBIND0R(RID, space_create)
EXBIND2(space_set_active, RID, bool)
EXBIND1RC(bool, space_is_active, RID)
+ EXBIND2(space_step, RID, real_t)
+ EXBIND1(space_flush_queries, RID)
EXBIND3(space_set_param, RID, PS3DE::SpaceParameter, real_t)
EXBIND2RC(real_t, space_get_param, RID, PS3DE::SpaceParameter)
@@ -551,6 +553,7 @@ class PhysicsServer3DExtension : public PhysicsServer3D {
EXBIND0RC(bool, is_flushing_queries)
EXBIND1R(int, get_process_info, PS3DE::ProcessInfo)
+ EXBIND2R(int, space_get_last_process_info, RID, PS3DE::ProcessInfo)
PhysicsServer3DExtension();
~PhysicsServer3DExtension();
diff --git a/servers/physics_3d/physics_server_3d_wrap_mt.h b/servers/physics_3d/physics_server_3d_wrap_mt.h
index dd41ff89a26c..180f28f02bd4 100644
--- a/servers/physics_3d/physics_server_3d_wrap_mt.h
+++ b/servers/physics_3d/physics_server_3d_wrap_mt.h
@@ -113,6 +113,8 @@ class PhysicsServer3DWrapMT : public PhysicsServer3D {
FUNCRID(space);
FUNC2(space_set_active, RID, bool);
FUNC1RC(bool, space_is_active, RID);
+ FUNC2(space_step, RID, real_t);
+ FUNC1(space_flush_queries, RID);
FUNC3(space_set_param, RID, PS3DE::SpaceParameter, real_t);
FUNC2RC(real_t, space_get_param, RID, PS3DE::SpaceParameter);
@@ -414,6 +416,10 @@ class PhysicsServer3DWrapMT : public PhysicsServer3D {
return physics_server_3d->get_process_info(p_info);
}
+ int space_get_last_process_info(RID p_space, PS3DE::ProcessInfo p_info) override {
+ return physics_server_3d->space_get_last_process_info(p_space, p_info);
+ }
+
PhysicsServer3DWrapMT(PhysicsServer3D *p_contained, bool p_create_thread);
~PhysicsServer3DWrapMT();
};
diff --git a/tests/servers/test_physics_server_manual_step.cpp b/tests/servers/test_physics_server_manual_step.cpp
new file mode 100644
index 000000000000..a723d7902339
--- /dev/null
+++ b/tests/servers/test_physics_server_manual_step.cpp
@@ -0,0 +1,291 @@
+/**************************************************************************/
+/* test_physics_server_manual_step.cpp */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#include "tests/test_macros.h"
+
+TEST_FORCE_LINK(test_physics_server_manual_step)
+
+#include "modules/modules_enabled.gen.h"
+
+// The test harness installs the dummy physics servers, so these cases construct
+// the real backends directly rather than going through PhysicsServer*DManager.
+
+#ifdef MODULE_GODOT_PHYSICS_2D_ENABLED
+#include "modules/godot_physics_2d/godot_physics_server_2d.h"
+#endif
+
+#ifdef MODULE_GODOT_PHYSICS_3D_ENABLED
+#include "modules/godot_physics_3d/godot_physics_server_3d.h"
+#endif
+
+namespace TestPhysicsServerManualStep {
+
+#ifdef MODULE_GODOT_PHYSICS_2D_ENABLED
+
+TEST_CASE("[PhysicsServer2D] Manually stepping an inactive space advances it") {
+ GodotPhysicsServer2D server;
+ server.init();
+
+ const RID space = server.space_create();
+ // Deliberately left inactive: an active space is stepped by the engine instead.
+ const RID body = server.body_create();
+ server.body_set_space(body, space);
+ server.body_set_mode(body, PS2DE::BODY_MODE_RIGID);
+
+ const RID shape = server.circle_shape_create();
+ server.shape_set_data(shape, 0.5);
+ server.body_add_shape(body, shape);
+
+ server.body_set_state(body, PS2DE::BODY_STATE_LINEAR_VELOCITY, Vector2(4, 0));
+
+ const Transform2D before = server.body_get_state(body, PS2DE::BODY_STATE_TRANSFORM);
+ for (int i = 0; i < 10; i++) {
+ server.space_step(space, 1.0 / 60.0);
+ }
+ const Transform2D after = server.body_get_state(body, PS2DE::BODY_STATE_TRANSFORM);
+
+ CHECK_MESSAGE(after.get_origin().x > before.get_origin().x,
+ "A body in a manually stepped space should move.");
+
+ server.free(shape);
+ server.free(body);
+ server.free(space);
+ server.finish();
+}
+
+TEST_CASE("[PhysicsServer2D] An inactive space only advances when stepped") {
+ GodotPhysicsServer2D server;
+ server.init();
+
+ const RID space = server.space_create();
+ const RID body = server.body_create();
+ server.body_set_space(body, space);
+ server.body_set_mode(body, PS2DE::BODY_MODE_RIGID);
+
+ const RID shape = server.circle_shape_create();
+ server.shape_set_data(shape, 0.5);
+ server.body_add_shape(body, shape);
+
+ server.body_set_state(body, PS2DE::BODY_STATE_LINEAR_VELOCITY, Vector2(4, 0));
+
+ const Transform2D before = server.body_get_state(body, PS2DE::BODY_STATE_TRANSFORM);
+ // No space_step() call here.
+ const Transform2D after = server.body_get_state(body, PS2DE::BODY_STATE_TRANSFORM);
+
+ CHECK_MESSAGE(after.get_origin().is_equal_approx(before.get_origin()),
+ "A body should not move while its space is neither active nor stepped.");
+
+ server.free(shape);
+ server.free(body);
+ server.free(space);
+ server.finish();
+}
+
+TEST_CASE("[PhysicsServer2D] Flushing queries with an invalid space keeps the flushing flag clear") {
+ GodotPhysicsServer2D server;
+ server.init();
+
+ CHECK_FALSE(server.is_flushing_queries());
+
+ ERR_PRINT_OFF;
+ server.space_flush_queries(RID());
+ ERR_PRINT_ON;
+
+ // Raising the flag before validating the argument would leave the server
+ // permanently "flushing", which silently disables every guard that reads
+ // is_flushing_queries() (such as Area2D::set_monitorable).
+ CHECK_MESSAGE(!server.is_flushing_queries(),
+ "An invalid space must not leave the server stuck in the flushing state.");
+
+ server.finish();
+}
+
+TEST_CASE("[PhysicsServer2D] Manually stepping an active space is rejected") {
+ GodotPhysicsServer2D server;
+ server.init();
+
+ const RID space = server.space_create();
+ server.space_set_active(space, true);
+
+ const RID body = server.body_create();
+ server.body_set_space(body, space);
+ server.body_set_mode(body, PS2DE::BODY_MODE_RIGID);
+
+ const RID shape = server.circle_shape_create();
+ server.shape_set_data(shape, 0.5);
+ server.body_add_shape(body, shape);
+
+ server.body_set_state(body, PS2DE::BODY_STATE_LINEAR_VELOCITY, Vector2(4, 0));
+
+ const Transform2D before = server.body_get_state(body, PS2DE::BODY_STATE_TRANSFORM);
+ ERR_PRINT_OFF;
+ server.space_step(space, 1.0 / 60.0);
+ server.space_flush_queries(space);
+ ERR_PRINT_ON;
+ const Transform2D after = server.body_get_state(body, PS2DE::BODY_STATE_TRANSFORM);
+
+ CHECK_MESSAGE(after.get_origin().is_equal_approx(before.get_origin()),
+ "An active space must not be advanced by a manual step.");
+ CHECK_FALSE(server.is_flushing_queries());
+
+ server.space_set_active(space, false);
+ server.free(shape);
+ server.free(body);
+ server.free(space);
+ server.finish();
+}
+
+#endif // MODULE_GODOT_PHYSICS_2D_ENABLED
+
+#ifdef MODULE_GODOT_PHYSICS_3D_ENABLED
+
+TEST_CASE("[PhysicsServer3D] Manually stepping an inactive space advances it") {
+ GodotPhysicsServer3D server;
+ server.init();
+
+ const RID space = server.space_create();
+ const RID body = server.body_create();
+ server.body_set_space(body, space);
+ server.body_set_mode(body, PS3DE::BODY_MODE_RIGID);
+
+ const RID shape = server.sphere_shape_create();
+ server.shape_set_data(shape, 0.5);
+ server.body_add_shape(body, shape);
+
+ server.body_set_state(body, PS3DE::BODY_STATE_LINEAR_VELOCITY, Vector3(4, 0, 0));
+
+ const Transform3D before = server.body_get_state(body, PS3DE::BODY_STATE_TRANSFORM);
+ for (int i = 0; i < 10; i++) {
+ server.space_step(space, 1.0 / 60.0);
+ }
+ const Transform3D after = server.body_get_state(body, PS3DE::BODY_STATE_TRANSFORM);
+
+ CHECK_MESSAGE(after.origin.x > before.origin.x,
+ "A body in a manually stepped space should move.");
+
+ server.free(shape);
+ server.free(body);
+ server.free(space);
+ server.finish();
+}
+
+TEST_CASE("[PhysicsServer3D] Stepping one space leaves another untouched") {
+ GodotPhysicsServer3D server;
+ server.init();
+
+ const RID shape = server.sphere_shape_create();
+ server.shape_set_data(shape, 0.5);
+
+ const RID space_a = server.space_create();
+ const RID body_a = server.body_create();
+ server.body_set_space(body_a, space_a);
+ server.body_set_mode(body_a, PS3DE::BODY_MODE_RIGID);
+ server.body_add_shape(body_a, shape);
+ server.body_set_state(body_a, PS3DE::BODY_STATE_LINEAR_VELOCITY, Vector3(4, 0, 0));
+
+ const RID space_b = server.space_create();
+ const RID body_b = server.body_create();
+ server.body_set_space(body_b, space_b);
+ server.body_set_mode(body_b, PS3DE::BODY_MODE_RIGID);
+ server.body_add_shape(body_b, shape);
+ server.body_set_state(body_b, PS3DE::BODY_STATE_LINEAR_VELOCITY, Vector3(4, 0, 0));
+
+ const Vector3 b_before = server.body_get_state(body_b, PS3DE::BODY_STATE_TRANSFORM).operator Transform3D().origin;
+ for (int i = 0; i < 10; i++) {
+ server.space_step(space_a, 1.0 / 60.0);
+ }
+ const Vector3 a_after = server.body_get_state(body_a, PS3DE::BODY_STATE_TRANSFORM).operator Transform3D().origin;
+ const Vector3 b_after = server.body_get_state(body_b, PS3DE::BODY_STATE_TRANSFORM).operator Transform3D().origin;
+
+ CHECK_MESSAGE(a_after.x > 0, "The stepped space should have advanced.");
+ CHECK_MESSAGE(b_after.is_equal_approx(b_before), "An unstepped space must not advance.");
+
+ server.free(body_a);
+ server.free(space_a);
+ server.free(body_b);
+ server.free(space_b);
+ server.free(shape);
+ server.finish();
+}
+
+TEST_CASE("[PhysicsServer3D] Flushing queries with an invalid space keeps the flushing flag clear") {
+ GodotPhysicsServer3D server;
+ server.init();
+
+ CHECK_FALSE(server.is_flushing_queries());
+
+ ERR_PRINT_OFF;
+ server.space_flush_queries(RID());
+ ERR_PRINT_ON;
+
+ CHECK_MESSAGE(!server.is_flushing_queries(),
+ "An invalid space must not leave the server stuck in the flushing state.");
+
+ server.finish();
+}
+
+TEST_CASE("[PhysicsServer3D] Manually stepping an active space is rejected") {
+ GodotPhysicsServer3D server;
+ server.init();
+
+ const RID space = server.space_create();
+ server.space_set_active(space, true);
+
+ const RID body = server.body_create();
+ server.body_set_space(body, space);
+ server.body_set_mode(body, PS3DE::BODY_MODE_RIGID);
+
+ const RID shape = server.sphere_shape_create();
+ server.shape_set_data(shape, 0.5);
+ server.body_add_shape(body, shape);
+
+ server.body_set_state(body, PS3DE::BODY_STATE_LINEAR_VELOCITY, Vector3(4, 0, 0));
+
+ const Transform3D before = server.body_get_state(body, PS3DE::BODY_STATE_TRANSFORM);
+ ERR_PRINT_OFF;
+ server.space_step(space, 1.0 / 60.0);
+ server.space_flush_queries(space);
+ ERR_PRINT_ON;
+ const Transform3D after = server.body_get_state(body, PS3DE::BODY_STATE_TRANSFORM);
+
+ CHECK_MESSAGE(after.origin.is_equal_approx(before.origin),
+ "An active space must not be advanced by a manual step.");
+ CHECK_FALSE(server.is_flushing_queries());
+
+ server.space_set_active(space, false);
+ server.free(shape);
+ server.free(body);
+ server.free(space);
+ server.finish();
+}
+
+#endif // MODULE_GODOT_PHYSICS_3D_ENABLED
+
+} // namespace TestPhysicsServerManualStep