From 40276d65110c45b4e9e03ac5808ae3fd43ad41f3 Mon Sep 17 00:00:00 2001 From: Daylily-Zeleen Date: Sun, 22 Feb 2026 15:37:23 +0800 Subject: [PATCH 1/3] Add space_step() to step physics simulation manually --- doc/classes/PhysicsServer2D.xml | 24 ++++++++ doc/classes/PhysicsServer2DExtension.xml | 20 +++++++ doc/classes/PhysicsServer3D.xml | 24 ++++++++ doc/classes/PhysicsServer3DExtension.xml | 20 +++++++ .../godot_physics_server_2d.cpp | 57 +++++++++++++++++++ .../godot_physics_server_2d.h | 3 + .../godot_physics_server_3d.cpp | 57 +++++++++++++++++++ .../godot_physics_server_3d.h | 3 + .../jolt_physics/jolt_physics_server_3d.cpp | 22 +++++++ modules/jolt_physics/jolt_physics_server_3d.h | 3 + servers/physics_2d/physics_server_2d.cpp | 3 + servers/physics_2d/physics_server_2d.h | 3 + servers/physics_2d/physics_server_2d_dummy.h | 3 + .../physics_server_2d_extension.cpp | 3 + .../physics_2d/physics_server_2d_extension.h | 3 + .../physics_2d/physics_server_2d_wrap_mt.h | 6 ++ servers/physics_3d/physics_server_3d.cpp | 3 + servers/physics_3d/physics_server_3d.h | 3 + servers/physics_3d/physics_server_3d_dummy.h | 3 + .../physics_server_3d_extension.cpp | 3 + .../physics_3d/physics_server_3d_extension.h | 3 + .../physics_3d/physics_server_3d_wrap_mt.h | 6 ++ 22 files changed, 275 insertions(+) diff --git a/doc/classes/PhysicsServer2D.xml b/doc/classes/PhysicsServer2D.xml index b16b1551fa24..661bce15107b 100644 --- a/doc/classes/PhysicsServer2D.xml +++ b/doc/classes/PhysicsServer2D.xml @@ -951,6 +951,13 @@ 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. + + + + + Flushes [param space]'s queries. It is necessary to call this method after calling [method space_step] with an active space from [code]_physics_process[/code]. Otherwise, call this method before calling [method space_step]. + + @@ -958,6 +965,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 +1005,15 @@ Sets the value of the given space parameter. + + + + + + Manually advance [param space] forward in [param delta]. This technique can be used for speeding up physics simulations, as seen in advanced rollback-style networking, or for predicting outcomes in scenarios such as hitting a ball in a billiards game. + [b]Note:[/b] If call this method with an active [param space] from [code]_physics_process()[/code], you should call [method space_flush_queries] afterwards. Otherwise, call [method space_flush_queries] beforehand. + + 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..8ed1ee59d6a8 100644 --- a/doc/classes/PhysicsServer3D.xml +++ b/doc/classes/PhysicsServer3D.xml @@ -1404,6 +1404,13 @@ 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]. + + + + + Flushes [param space]'s queries. It is necessary to call this method after calling [method space_step] with an active space from [code]_physics_process[/code]. Otherwise, call this method before calling [method space_step]. + + @@ -1411,6 +1418,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 +1458,15 @@ Sets the value for a space parameter. A list of available parameters is on the [enum SpaceParameter] constants. + + + + + + Manually advance [param space] forward in [param delta]. This technique can be used for speeding up physics simulations, as seen in advanced rollback-style networking, or for predicting outcomes in scenarios such as hitting a ball in a billiards game. + [b]Note:[/b] If call this method with an active [param space] from [code]_physics_process()[/code], you should call [method space_flush_queries] afterwards. Otherwise, call [method space_flush_queries] beforehand. + + 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..f652734d4782 100644 --- a/modules/godot_physics_2d/godot_physics_server_2d.cpp +++ b/modules/godot_physics_2d/godot_physics_server_2d.cpp @@ -244,6 +244,44 @@ 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), "A activate godot space can't be stepped manually."); + + // May be let pending_shape_update_list as a member of GodotSpaces3D and update shapes by themselves. + // To avoid effecting Spaces which are handled by developer (for lockstep/rollback netcode, it is particularly sensitive). + // Otherwise, call _update_shapes() directly. + 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) { + // Like _update_shapes(), to provide controllability for developers, flushing_queries flag should active as a member of space and check it for each space. + // But I'm not sure about that, I am not familiar with multi-threads and the architecture of GodotPhysics. + flushing_queries = true; + + GodotSpace2D *space = space_owner.get_or_null(p_space); + ERR_FAIL_NULL(space); + ERR_FAIL_COND_MSG(active_spaces.has(space), "A activate godot space should not flush queries manually."); + space->call_queries(); + + flushing_queries = false; +} + 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 +1426,25 @@ int GodotPhysicsServer2D::get_process_info(PS2DE::ProcessInfo p_info) { return 0; } +int GodotPhysicsServer2D::space_get_last_process_info(RID p_space, ProcessInfo p_info) { + GodotSpace2D *space = space_owner.get_or_null(p_space); + ERR_FAIL_NULL_V(space, 0); + + switch (p_info) { + case INFO_ACTIVE_OBJECTS: { + return space->get_active_objects(); + } break; + case INFO_COLLISION_PAIRS: { + return space->get_collision_pairs(); + } break; + case 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..302c404a7970 100644 --- a/modules/godot_physics_3d/godot_physics_server_3d.cpp +++ b/modules/godot_physics_3d/godot_physics_server_3d.cpp @@ -175,6 +175,44 @@ 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), "A activate godot space can't be stepped manually."); + + // May be let pending_shape_update_list as a member of GodotSpaces3D and update shapes by themselves. + // To avoid effecting Spaces which are handled by developer (for lockstep/rollback netcode, it is particularly sensitive). + // If it is unnecessary, call _update_shapes() directly. + 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) { + // Like _update_shapes(), to provide controllability for developers, flushing_queries flag should active as a member of space and check it for each space. + // But I'm not sure about that, I am not familiar with multi-threads and the architecture of GodotPhysics. + flushing_queries = true; + + GodotSpace3D *space = space_owner.get_or_null(p_space); + ERR_FAIL_NULL(space); + ERR_FAIL_COND_MSG(active_spaces.has(space), "A activate godot space should not flush queries manually."); + space->call_queries(); + + flushing_queries = false; +} + 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 +1824,25 @@ int GodotPhysicsServer3D::get_process_info(PS3DE::ProcessInfo p_info) { return 0; } +int GodotPhysicsServer3D::space_get_last_process_info(RID p_space, ProcessInfo p_info) { + GodotSpace3D *space = space_owner.get_or_null(p_space); + ERR_FAIL_NULL_V(space, 0); + + switch (p_info) { + case INFO_ACTIVE_OBJECTS: { + return space->get_active_objects(); + } break; + case INFO_COLLISION_PAIRS: { + return space->get_collision_pairs(); + } break; + case 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..4b3cd7fdf4aa 100644 --- a/modules/jolt_physics/jolt_physics_server_3d.cpp +++ b/modules/jolt_physics/jolt_physics_server_3d.cpp @@ -214,6 +214,24 @@ 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()); + + space->step(p_delta); +} + +void JoltPhysicsServer3D::space_flush_queries(RID p_space) { + flushing_queries = true; + + JoltSpace3D *space = space_owner.get_or_null(p_space); + ERR_FAIL_NULL(space); + space->call_queries(); + + flushing_queries = false; +} + 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 +1706,10 @@ int JoltPhysicsServer3D::get_process_info(PS3DE::ProcessInfo p_process_info) { return 0; } +int JoltPhysicsServer3D::space_get_last_process_info(RID p_space, 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..df25d0099e75 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..aa1ee5a92662 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(); }; From 7469cfd24e0b432dfca3be6b3f9d0b73b79546a5 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 03:24:07 +0000 Subject: [PATCH 2/3] Physics: Harden manual space stepping and align backends Review fixes on top of the manual space-stepping API: - Fix `flushing_queries` being raised before argument validation. An invalid RID returned early with the flag still set, leaving the server permanently in the "flushing" state and silently disabling every guard that reads `is_flushing_queries()` (such as `Area2D::set_monitorable`). The flag is now saved and restored around the call rather than forced to `false`, so a nested flush from inside a physics callback cannot clear the outer one. - Bracket the Jolt backend's `space_step()` with `job_system->pre_step()` and `post_step()`, matching `step()`. Without this, repeated manual steps exhaust Jolt's job allocator. - Reject active spaces in the Jolt backend, matching the GodotPhysics backends. An active space is already advanced once per physics frame, so stepping it manually double-advanced the simulation. - Correct the documentation, which described stepping an active space from `_physics_process()` as a supported workflow while every backend rejects it. - Qualify `ProcessInfo` with the enum namespace in the backend definitions, and restore the trailing semicolons on two `GDVIRTUAL_BIND` lines. --- doc/classes/PhysicsServer2D.xml | 8 +++-- doc/classes/PhysicsServer3D.xml | 8 +++-- .../godot_physics_server_2d.cpp | 32 +++++++++++-------- .../godot_physics_server_3d.cpp | 32 +++++++++++-------- .../jolt_physics/jolt_physics_server_3d.cpp | 24 +++++++++++--- .../physics_server_2d_extension.cpp | 4 +-- .../physics_server_3d_extension.cpp | 4 +-- 7 files changed, 69 insertions(+), 43 deletions(-) diff --git a/doc/classes/PhysicsServer2D.xml b/doc/classes/PhysicsServer2D.xml index 661bce15107b..d7bd46590635 100644 --- a/doc/classes/PhysicsServer2D.xml +++ b/doc/classes/PhysicsServer2D.xml @@ -955,7 +955,8 @@ - Flushes [param space]'s queries. It is necessary to call this method after calling [method space_step] with an active space from [code]_physics_process[/code]. Otherwise, call this method before calling [method space_step]. + 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. @@ -1010,8 +1011,9 @@ - Manually advance [param space] forward in [param delta]. This technique can be used for speeding up physics simulations, as seen in advanced rollback-style networking, or for predicting outcomes in scenarios such as hitting a ball in a billiards game. - [b]Note:[/b] If call this method with an active [param space] from [code]_physics_process()[/code], you should call [method space_flush_queries] afterwards. Otherwise, call [method space_flush_queries] beforehand. + 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/PhysicsServer3D.xml b/doc/classes/PhysicsServer3D.xml index 8ed1ee59d6a8..dc2bc6b4e1fa 100644 --- a/doc/classes/PhysicsServer3D.xml +++ b/doc/classes/PhysicsServer3D.xml @@ -1408,7 +1408,8 @@ - Flushes [param space]'s queries. It is necessary to call this method after calling [method space_step] with an active space from [code]_physics_process[/code]. Otherwise, call this method before calling [method space_step]. + 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. @@ -1463,8 +1464,9 @@ - Manually advance [param space] forward in [param delta]. This technique can be used for speeding up physics simulations, as seen in advanced rollback-style networking, or for predicting outcomes in scenarios such as hitting a ball in a billiards game. - [b]Note:[/b] If call this method with an active [param space] from [code]_physics_process()[/code], you should call [method space_flush_queries] afterwards. Otherwise, call [method space_flush_queries] beforehand. + 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/modules/godot_physics_2d/godot_physics_server_2d.cpp b/modules/godot_physics_2d/godot_physics_server_2d.cpp index f652734d4782..5a66e74a7dbf 100644 --- a/modules/godot_physics_2d/godot_physics_server_2d.cpp +++ b/modules/godot_physics_2d/godot_physics_server_2d.cpp @@ -247,11 +247,11 @@ bool GodotPhysicsServer2D::space_is_active(RID p_space) const { 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), "A activate godot space can't be stepped manually."); + ERR_FAIL_COND_MSG(active_spaces.has(space), "An active space can't be stepped manually. Deactivate it with space_set_active() first."); - // May be let pending_shape_update_list as a member of GodotSpaces3D and update shapes by themselves. - // To avoid effecting Spaces which are handled by developer (for lockstep/rollback netcode, it is particularly sensitive). - // Otherwise, call _update_shapes() directly. + // 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) { @@ -270,16 +270,20 @@ void GodotPhysicsServer2D::space_step(RID p_space, real_t p_delta) { } void GodotPhysicsServer2D::space_flush_queries(RID p_space) { - // Like _update_shapes(), to provide controllability for developers, flushing_queries flag should active as a member of space and check it for each space. - // But I'm not sure about that, I am not familiar with multi-threads and the architecture of GodotPhysics. - flushing_queries = true; - GodotSpace2D *space = space_owner.get_or_null(p_space); ERR_FAIL_NULL(space); - ERR_FAIL_COND_MSG(active_spaces.has(space), "A activate godot space should not flush queries manually."); + 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 = false; + flushing_queries = was_flushing_queries; } void GodotPhysicsServer2D::space_set_param(RID p_space, PS2DE::SpaceParameter p_param, real_t p_value) { @@ -1426,18 +1430,18 @@ int GodotPhysicsServer2D::get_process_info(PS2DE::ProcessInfo p_info) { return 0; } -int GodotPhysicsServer2D::space_get_last_process_info(RID p_space, ProcessInfo p_info) { +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 INFO_ACTIVE_OBJECTS: { + case PS2DE::INFO_ACTIVE_OBJECTS: { return space->get_active_objects(); } break; - case INFO_COLLISION_PAIRS: { + case PS2DE::INFO_COLLISION_PAIRS: { return space->get_collision_pairs(); } break; - case INFO_ISLAND_COUNT: { + case PS2DE::INFO_ISLAND_COUNT: { return space->get_island_count(); } break; } diff --git a/modules/godot_physics_3d/godot_physics_server_3d.cpp b/modules/godot_physics_3d/godot_physics_server_3d.cpp index 302c404a7970..0181bee4a385 100644 --- a/modules/godot_physics_3d/godot_physics_server_3d.cpp +++ b/modules/godot_physics_3d/godot_physics_server_3d.cpp @@ -178,11 +178,11 @@ bool GodotPhysicsServer3D::space_is_active(RID p_space) const { 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), "A activate godot space can't be stepped manually."); + ERR_FAIL_COND_MSG(active_spaces.has(space), "An active space can't be stepped manually. Deactivate it with space_set_active() first."); - // May be let pending_shape_update_list as a member of GodotSpaces3D and update shapes by themselves. - // To avoid effecting Spaces which are handled by developer (for lockstep/rollback netcode, it is particularly sensitive). - // If it is unnecessary, call _update_shapes() directly. + // 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) { @@ -201,16 +201,20 @@ void GodotPhysicsServer3D::space_step(RID p_space, real_t p_delta) { } void GodotPhysicsServer3D::space_flush_queries(RID p_space) { - // Like _update_shapes(), to provide controllability for developers, flushing_queries flag should active as a member of space and check it for each space. - // But I'm not sure about that, I am not familiar with multi-threads and the architecture of GodotPhysics. - flushing_queries = true; - GodotSpace3D *space = space_owner.get_or_null(p_space); ERR_FAIL_NULL(space); - ERR_FAIL_COND_MSG(active_spaces.has(space), "A activate godot space should not flush queries manually."); + 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 = false; + flushing_queries = was_flushing_queries; } void GodotPhysicsServer3D::space_set_param(RID p_space, PS3DE::SpaceParameter p_param, real_t p_value) { @@ -1824,18 +1828,18 @@ int GodotPhysicsServer3D::get_process_info(PS3DE::ProcessInfo p_info) { return 0; } -int GodotPhysicsServer3D::space_get_last_process_info(RID p_space, ProcessInfo p_info) { +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 INFO_ACTIVE_OBJECTS: { + case PS3DE::INFO_ACTIVE_OBJECTS: { return space->get_active_objects(); } break; - case INFO_COLLISION_PAIRS: { + case PS3DE::INFO_COLLISION_PAIRS: { return space->get_collision_pairs(); } break; - case INFO_ISLAND_COUNT: { + case PS3DE::INFO_ISLAND_COUNT: { return space->get_island_count(); } break; } diff --git a/modules/jolt_physics/jolt_physics_server_3d.cpp b/modules/jolt_physics/jolt_physics_server_3d.cpp index 4b3cd7fdf4aa..4683badb8589 100644 --- a/modules/jolt_physics/jolt_physics_server_3d.cpp +++ b/modules/jolt_physics/jolt_physics_server_3d.cpp @@ -218,18 +218,32 @@ 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."); - space->step(p_delta); + // 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) { - flushing_queries = true; - 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 = false; + flushing_queries = was_flushing_queries; } void JoltPhysicsServer3D::space_set_param(RID p_space, PS3DE::SpaceParameter p_param, real_t p_value) { @@ -1706,7 +1720,7 @@ int JoltPhysicsServer3D::get_process_info(PS3DE::ProcessInfo p_process_info) { return 0; } -int JoltPhysicsServer3D::space_get_last_process_info(RID p_space, ProcessInfo p_info) { +int JoltPhysicsServer3D::space_get_last_process_info(RID p_space, PS3DE::ProcessInfo p_info) { return 0; } diff --git a/servers/physics_2d/physics_server_2d_extension.cpp b/servers/physics_2d/physics_server_2d_extension.cpp index df25d0099e75..fb2aabeb1392 100644 --- a/servers/physics_2d/physics_server_2d_extension.cpp +++ b/servers/physics_2d/physics_server_2d_extension.cpp @@ -159,8 +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_step, "space", "delta"); + GDVIRTUAL_BIND(_space_flush_queries, "space"); GDVIRTUAL_BIND(_space_set_param, "space", "param", "value"); GDVIRTUAL_BIND(_space_get_param, "space", "param"); diff --git a/servers/physics_3d/physics_server_3d_extension.cpp b/servers/physics_3d/physics_server_3d_extension.cpp index aa1ee5a92662..170f8d86da3b 100644 --- a/servers/physics_3d/physics_server_3d_extension.cpp +++ b/servers/physics_3d/physics_server_3d_extension.cpp @@ -165,8 +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_step, "space", "delta"); + GDVIRTUAL_BIND(_space_flush_queries, "space"); GDVIRTUAL_BIND(_space_set_param, "space", "param", "value"); GDVIRTUAL_BIND(_space_get_param, "space", "param"); From a420c195a5dcd80960ca7b077e0b7e67a2dfdf36 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 03:32:49 +0000 Subject: [PATCH 3/3] Tests: Cover manual physics space stepping Adds regression coverage for the manual stepping API against the real GodotPhysics backends. The test harness installs the dummy servers, so these cases construct `GodotPhysicsServer2D`/`3D` directly instead of going through the server managers. Covered: a manually stepped inactive space advances; an unstepped space does not; stepping one space leaves another untouched; an active space rejects a manual step; and flushing queries with an invalid space leaves the server's flushing flag clear. The last case fails without the accompanying validation-ordering fix. --- .../test_physics_server_manual_step.cpp | 291 ++++++++++++++++++ 1 file changed, 291 insertions(+) create mode 100644 tests/servers/test_physics_server_manual_step.cpp 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