Skip to content

PhysicsDirectSpaceState2D/3D: add typed result objects - #113970

Open
aterray wants to merge 12 commits into
godotengine:masterfrom
aterray:feature/directspace-typed-results
Open

PhysicsDirectSpaceState2D/3D: add typed result objects#113970
aterray wants to merge 12 commits into
godotengine:masterfrom
aterray:feature/directspace-typed-results

Conversation

@aterray

@aterray aterray commented Dec 12, 2025

Copy link
Copy Markdown

This PR adds new query methods to PhysicsDirectSpaceState2D/3D that write results into typed result objects. This avoids allocating dictionary and array results for each query, improving performance.

New API

Methods

The following methods are added to PhysicsDirectSpaceState2D:

  • bool intersect_ray_typed(parameters: PhysicsRayQueryParameters2D, result: PhysicsRayIntersectionResult2D)
  • bool intersect_point_typed(parameters: PhysicsPointQueryParameters2D, result: PhysicsPointIntersectionResult2D)
  • bool intersect_shape_typed(parameters: PhysicsShapeQueryParameters2D, result: PhysicsShapeIntersectionResult2D)
  • bool cast_motion_typed(parameters: PhysicsShapeQueryParameters2D, result: PhysicsCastMotionResult2D)
  • bool collide_shape_typed(parameters: PhysicsShapeQueryParameters2D, result: PhysicsShapeCollisionResult2D)
  • bool get_rest_info_typed(parameters: PhysicsShapeQueryParameters2D, result: PhysicsRestInfoResult2D)

The equivalent methods are added to PhysicsDirectSpaceState3D.

Classes

  • PhysicsRayIntersectionResult2D/3D
  • PhysicsPointIntersectionResult2D/3D
  • PhysicsShapeIntersectionResult2D/3D
  • PhysicsCastMotionResult2D/3D
  • PhysicsShapeCollisionResult2D/3D
  • PhysicsRestInfoResult2D/3D

Motivation

  1. Performance

Result objects can be reused to avoid dictionary and array allocations on every call:

var result := PhysicsCastMotionResult3D.new()

func _physics_process(delta: float) -> void:
    if direct_space_state.cast_motion_typed(params, result):
        ...
  1. Strongly typed access

The new result objects eliminate the need for manual type declarations and dictionary lookups:

var result := PhysicsRayIntersectionResult3D.new()

if direct_space_state.intersect_ray_typed(params, result):
    var normal := result.get_normal()
    var position := result.get_position()

The existing methods require dictionary lookups and explicit type annotations:

var result := direct_space_state.intersect_ray(params)

if result:
    # Cannot infer the type of "normal"
    # var normal := result["normal"]
    var normal: Vector3 = result["normal"]
  1. Consistency

This follows the same approach as body_test_motion() in PhysicsServer2D/3D, which uses PhysicsTestMotionParameters2D/3D and PhysicsTestMotionResult2D/3D.

Microbenchmarks and Equivalence Tests

Environment
  • Godot: v4.8.dev.custom_build.1fc32ac76
  • CPU: Intel(R) Core(TM) i9-9980HK
  • OS: Windows 10
  • Build: scons -j16 d3d12=no accesskit=no platform=windows target=editor production=yes optimize=speed
  • Run: godot --headless --path .
Script
# NOTE: Autoload

extends Node

const SAMPLES := 1_000
const ITERATIONS := 1_000

static func _intersect_ray_typed_test_2d(space_state: PhysicsDirectSpaceState2D, params: PhysicsRayQueryParameters2D) -> void:
    var result_a := space_state.intersect_ray(params)
    var result_b := PhysicsRayIntersectionResult2D.new()
    space_state.intersect_ray_typed(params, result_b)
    assert(result_b.collider == result_a["collider"])
    assert(result_b.collider_id == result_a["collider_id"])
    assert(result_b.normal.is_equal_approx(result_a["normal"]))
    assert(result_b.position.is_equal_approx(result_a["position"]))
    assert(result_b.collider_rid == result_a["rid"])
    assert(result_b.collider_shape == result_a["shape"])

static func _intersect_point_typed_test_2d(space_state: PhysicsDirectSpaceState2D, params: PhysicsPointQueryParameters2D) -> void:
    var result_a := space_state.intersect_point(params)
    var result_b := PhysicsPointIntersectionResult2D.new()
    space_state.intersect_point_typed(params, result_b)
    assert(result_b.collision_count == result_a.size())
    for i in result_b.collision_count:
        assert(result_b.get_collider(i) == result_a[i]["collider"])
        assert(result_b.get_collider_id(i) == result_a[i]["collider_id"])    
        assert(result_b.get_collider_rid(i) == result_a[i]["rid"])
        assert(result_b.get_collider_shape(i) == result_a[i]["shape"])

static func _intersect_shape_typed_test_2d(space_state: PhysicsDirectSpaceState2D, params: PhysicsShapeQueryParameters2D) -> void:
    var result_a := space_state.intersect_shape(params)
    var result_b := PhysicsShapeIntersectionResult2D.new()
    space_state.intersect_shape_typed(params, result_b)
    assert(result_b.collision_count == result_a.size())
    for i in result_b.collision_count:
        assert(result_b.get_collider(i) == result_a[i]["collider"])
        assert(result_b.get_collider_id(i) == result_a[i]["collider_id"])    
        assert(result_b.get_collider_rid(i) == result_a[i]["rid"])
        assert(result_b.get_collider_shape(i) == result_a[i]["shape"])

static func _cast_motion_typed_test_2d(space_state: PhysicsDirectSpaceState2D, params: PhysicsShapeQueryParameters2D) -> void:
    var result_a := space_state.cast_motion(params)
    var result_b := PhysicsCastMotionResult2D.new()
    space_state.cast_motion_typed(params, result_b)
    assert(is_equal_approx(result_b.safe_fraction, result_a[0]))
    assert(is_equal_approx(result_b.unsafe_fraction, result_a[1]))

static func _collide_shape_typed_test_2d(space_state: PhysicsDirectSpaceState2D, params: PhysicsShapeQueryParameters2D) -> void:
    var result_a := space_state.collide_shape(params)
    var result_b := PhysicsShapeCollisionResult2D.new()
    space_state.collide_shape_typed(params, result_b)
    assert(2*result_b.collision_count == result_a.size())
    for i in result_b.collision_count:
        assert(result_b.get_point_on_queried_shape(i).is_equal_approx(result_a[2*i]))
        assert(result_b.get_point_on_colliding_shape(i).is_equal_approx(result_a[2*i+1]))

static func _get_rest_info_typed_test_2d(space_state: PhysicsDirectSpaceState2D, params: PhysicsShapeQueryParameters2D) -> void:
    var result_a := space_state.get_rest_info(params)
    var result_b := PhysicsRestInfoResult2D.new()
    space_state.get_rest_info_typed(params, result_b)
    assert(result_b.collider_id == result_a["collider_id"])
    assert(result_b.collider_velocity == result_a["linear_velocity"])
    assert(result_b.normal.is_equal_approx(result_a["normal"]))
    assert(result_b.point.is_equal_approx(result_a["point"]))
    assert(result_b.collider_rid == result_a["rid"])
    assert(result_b.collider_shape == result_a["shape"])

static func _intersect_ray_time_2d(space_state: PhysicsDirectSpaceState2D, params: PhysicsRayQueryParameters2D) -> float:    
    var total := 0
    for s in SAMPLES:
        var start := Time.get_ticks_usec()
        for i in ITERATIONS: 
            space_state.intersect_ray(params)
        var elapsed := Time.get_ticks_usec() - start
        total += elapsed
    return total / float(SAMPLES * ITERATIONS)

static func _intersect_ray_typed_time_2d(space_state: PhysicsDirectSpaceState2D, params: PhysicsRayQueryParameters2D) -> float:
    var result := PhysicsRayIntersectionResult2D.new()
    var total := 0
    for s in SAMPLES:
        var start := Time.get_ticks_usec()
        for i in ITERATIONS: 
            space_state.intersect_ray_typed(params, result)
        var elapsed := Time.get_ticks_usec() - start
        total += elapsed
    return total / float(SAMPLES * ITERATIONS)

static func _intersect_point_time_2d(space_state: PhysicsDirectSpaceState2D, params: PhysicsPointQueryParameters2D) -> float:    
    var total := 0
    for s in SAMPLES:
        var start := Time.get_ticks_usec()
        for i in ITERATIONS: 
            space_state.intersect_point(params)
        var elapsed := Time.get_ticks_usec() - start
        total += elapsed
    return total / float(SAMPLES * ITERATIONS)

static func _intersect_point_typed_time_2d(space_state: PhysicsDirectSpaceState2D, params: PhysicsPointQueryParameters2D) -> float:    
    var result := PhysicsPointIntersectionResult2D.new()
    var total := 0
    for s in SAMPLES:
        var start := Time.get_ticks_usec()
        for i in ITERATIONS: 
            space_state.intersect_point_typed(params, result)
        var elapsed := Time.get_ticks_usec() - start
        total += elapsed
    return total / float(SAMPLES * ITERATIONS)

static func _intersect_shape_time_2d(space_state: PhysicsDirectSpaceState2D, params: PhysicsShapeQueryParameters2D) -> float:    
    var total := 0
    for s in SAMPLES:
        var start := Time.get_ticks_usec()
        for i in ITERATIONS: 
            space_state.intersect_shape(params)
        var elapsed := Time.get_ticks_usec() - start
        total += elapsed
    return total / float(SAMPLES * ITERATIONS)

static func _intersect_shape_typed_time_2d(space_state: PhysicsDirectSpaceState2D, params: PhysicsShapeQueryParameters2D) -> float:    
    var result := PhysicsShapeIntersectionResult2D.new()
    var total := 0
    for s in SAMPLES:
        var start := Time.get_ticks_usec()
        for i in ITERATIONS: 
            space_state.intersect_shape_typed(params, result)
        var elapsed := Time.get_ticks_usec() - start
        total += elapsed
    return total / float(SAMPLES * ITERATIONS)

static func _cast_motion_time_2d(space_state: PhysicsDirectSpaceState2D, params: PhysicsShapeQueryParameters2D) -> float:
    var total := 0
    for s in SAMPLES:
        var start := Time.get_ticks_usec()
        for i in ITERATIONS: 
            space_state.cast_motion(params)
        var elapsed := Time.get_ticks_usec() - start
        total += elapsed
    return total / float(SAMPLES * ITERATIONS)

static func _cast_motion_typed_time_2d(space_state: PhysicsDirectSpaceState2D, params: PhysicsShapeQueryParameters2D) -> float:    
    var result := PhysicsCastMotionResult2D.new()
    var total := 0
    for s in SAMPLES:
        var start := Time.get_ticks_usec()
        for i in ITERATIONS: 
            space_state.cast_motion_typed(params, result)
        var elapsed := Time.get_ticks_usec() - start
        total += elapsed
    return total / float(SAMPLES * ITERATIONS)

static func _collide_shape_time_2d(space_state: PhysicsDirectSpaceState2D, params: PhysicsShapeQueryParameters2D) -> float:    
    var total := 0
    for s in SAMPLES:
        var start := Time.get_ticks_usec()
        for i in ITERATIONS: 
            space_state.collide_shape(params)
        var elapsed := Time.get_ticks_usec() - start
        total += elapsed
    return total / float(SAMPLES * ITERATIONS)

static func _collide_shape_typed_time_2d(space_state: PhysicsDirectSpaceState2D, params: PhysicsShapeQueryParameters2D) -> float:    
    var result := PhysicsShapeCollisionResult2D.new()
    var total := 0
    for s in SAMPLES:
        var start := Time.get_ticks_usec()
        for i in ITERATIONS: 
            space_state.collide_shape_typed(params, result)
        var elapsed := Time.get_ticks_usec() - start
        total += elapsed
    return total / float(SAMPLES * ITERATIONS)

static func _get_rest_info_time_2d(space_state: PhysicsDirectSpaceState2D, params: PhysicsShapeQueryParameters2D) -> float:    
    var total := 0
    for s in SAMPLES:
        var start := Time.get_ticks_usec()
        for i in ITERATIONS: 
            space_state.get_rest_info(params)
        var elapsed := Time.get_ticks_usec() - start
        total += elapsed
    return total / float(SAMPLES * ITERATIONS)

static func _get_rest_info_typed_time_2d(space_state: PhysicsDirectSpaceState2D, params: PhysicsShapeQueryParameters2D) -> float:    
    var result := PhysicsRestInfoResult2D.new()
    var total := 0
    for s in SAMPLES:
        var start := Time.get_ticks_usec()
        for i in ITERATIONS: 
            space_state.get_rest_info_typed(params, result)
        var elapsed := Time.get_ticks_usec() - start
        total += elapsed
    return total / float(SAMPLES * ITERATIONS)

static func _intersect_ray_typed_test_3d(space_state: PhysicsDirectSpaceState3D, params: PhysicsRayQueryParameters3D) -> void:
    var result_a := space_state.intersect_ray(params)
    var result_b := PhysicsRayIntersectionResult3D.new()
    space_state.intersect_ray_typed(params, result_b)
    assert(result_b.collider == result_a["collider"])
    assert(result_b.collider_id == result_a["collider_id"])
    assert(result_b.normal.is_equal_approx(result_a["normal"]))
    assert(result_b.position.is_equal_approx(result_a["position"]))
    assert(result_b.collider_face_id == result_a["face_index"])
    assert(result_b.collider_rid == result_a["rid"])
    assert(result_b.collider_shape == result_a["shape"])

static func _intersect_point_typed_test_3d(space_state: PhysicsDirectSpaceState3D, params: PhysicsPointQueryParameters3D) -> void:
    var result_a := space_state.intersect_point(params)
    var result_b := PhysicsPointIntersectionResult3D.new()
    space_state.intersect_point_typed(params, result_b)
    assert(result_b.collision_count == result_a.size())
    for i in result_b.collision_count:
        assert(result_b.get_collider(i) == result_a[i]["collider"])
        assert(result_b.get_collider_id(i) == result_a[i]["collider_id"])    
        assert(result_b.get_collider_rid(i) == result_a[i]["rid"])
        assert(result_b.get_collider_shape(i) == result_a[i]["shape"])
    
static func _intersect_shape_typed_test_3d(space_state: PhysicsDirectSpaceState3D, params: PhysicsShapeQueryParameters3D) -> void:
    var result_a := space_state.intersect_shape(params)
    var result_b := PhysicsShapeIntersectionResult3D.new()
    space_state.intersect_shape_typed(params, result_b)
    assert(result_b.collision_count == result_a.size())
    for i in result_b.collision_count:
        assert(result_b.get_collider(i) == result_a[i]["collider"])
        assert(result_b.get_collider_id(i) == result_a[i]["collider_id"])    
        assert(result_b.get_collider_rid(i) == result_a[i]["rid"])
        assert(result_b.get_collider_shape(i) == result_a[i]["shape"])

static func _cast_motion_typed_test_3d(space_state: PhysicsDirectSpaceState3D, params: PhysicsShapeQueryParameters3D) -> void:
    var result_a := space_state.cast_motion(params)
    var result_b := PhysicsCastMotionResult3D.new()
    space_state.cast_motion_typed(params, result_b)
    assert(is_equal_approx(result_b.safe_fraction, result_a[0]))
    assert(is_equal_approx(result_b.unsafe_fraction, result_a[1]))

static func _collide_shape_typed_test_3d(space_state: PhysicsDirectSpaceState3D, params: PhysicsShapeQueryParameters3D) -> void:
    var result_a := space_state.collide_shape(params)
    var result_b := PhysicsShapeCollisionResult3D.new()
    space_state.collide_shape_typed(params, result_b)
    assert(2*result_b.collision_count == result_a.size())
    for i in result_b.collision_count:
        assert(result_b.get_point_on_queried_shape(i).is_equal_approx(result_a[2*i]))
        assert(result_b.get_point_on_colliding_shape(i).is_equal_approx(result_a[2*i+1]))

static func _get_rest_info_typed_test_3d(space_state: PhysicsDirectSpaceState3D, params: PhysicsShapeQueryParameters3D) -> void:
    var result_a := space_state.get_rest_info(params)
    var result_b := PhysicsRestInfoResult3D.new()
    space_state.get_rest_info_typed(params, result_b)
    assert(result_b.collider_id == result_a["collider_id"])
    assert(result_b.collider_velocity == result_a["linear_velocity"])
    assert(result_b.normal.is_equal_approx(result_a["normal"]))
    assert(result_b.point.is_equal_approx(result_a["point"]))
    assert(result_b.collider_rid == result_a["rid"])
    assert(result_b.collider_shape == result_a["shape"])

static func _intersect_ray_time_3d(space_state: PhysicsDirectSpaceState3D, params: PhysicsRayQueryParameters3D) -> float:    
    var total := 0
    for s in SAMPLES:
        var start := Time.get_ticks_usec()
        for i in ITERATIONS: 
            space_state.intersect_ray(params)
        var elapsed := Time.get_ticks_usec() - start
        total += elapsed
    return total / float(SAMPLES * ITERATIONS)

static func _intersect_ray_typed_time_3d(space_state: PhysicsDirectSpaceState3D, params: PhysicsRayQueryParameters3D) -> float:
    var result := PhysicsRayIntersectionResult3D.new()
    var total := 0
    for s in SAMPLES:
        var start := Time.get_ticks_usec()
        for i in ITERATIONS: 
            space_state.intersect_ray_typed(params, result)
        var elapsed := Time.get_ticks_usec() - start
        total += elapsed
    return total / float(SAMPLES * ITERATIONS)

static func _intersect_point_time_3d(space_state: PhysicsDirectSpaceState3D, params: PhysicsPointQueryParameters3D) -> float:    
    var total := 0
    for s in SAMPLES:
        var start := Time.get_ticks_usec()
        for i in ITERATIONS: 
            space_state.intersect_point(params)
        var elapsed := Time.get_ticks_usec() - start
        total += elapsed
    return total / float(SAMPLES * ITERATIONS)

static func _intersect_point_typed_time_3d(space_state: PhysicsDirectSpaceState3D, params: PhysicsPointQueryParameters3D) -> float:    
    var result := PhysicsPointIntersectionResult3D.new()
    var total := 0
    for s in SAMPLES:
        var start := Time.get_ticks_usec()
        for i in ITERATIONS: 
            space_state.intersect_point_typed(params, result)
        var elapsed := Time.get_ticks_usec() - start
        total += elapsed
    return total / float(SAMPLES * ITERATIONS)

static func _intersect_shape_time_3d(space_state: PhysicsDirectSpaceState3D, params: PhysicsShapeQueryParameters3D) -> float:    
    var total := 0
    for s in SAMPLES:
        var start := Time.get_ticks_usec()
        for i in ITERATIONS: 
            space_state.intersect_shape(params)
        var elapsed := Time.get_ticks_usec() - start
        total += elapsed
    return total / float(SAMPLES * ITERATIONS)

static func _intersect_shape_typed_time_3d(space_state: PhysicsDirectSpaceState3D, params: PhysicsShapeQueryParameters3D) -> float:    
    var result := PhysicsShapeIntersectionResult3D.new()
    var total := 0
    for s in SAMPLES:
        var start := Time.get_ticks_usec()
        for i in ITERATIONS: 
            space_state.intersect_shape_typed(params, result)
        var elapsed := Time.get_ticks_usec() - start
        total += elapsed
    return total / float(SAMPLES * ITERATIONS)

static func _cast_motion_time_3d(space_state: PhysicsDirectSpaceState3D, params: PhysicsShapeQueryParameters3D) -> float:
    var total := 0
    for s in SAMPLES:
        var start := Time.get_ticks_usec()
        for i in ITERATIONS: 
            space_state.cast_motion(params)
        var elapsed := Time.get_ticks_usec() - start
        total += elapsed
    return total / float(SAMPLES * ITERATIONS)

static func _cast_motion_typed_time_3d(space_state: PhysicsDirectSpaceState3D, params: PhysicsShapeQueryParameters3D) -> float:    
    var result := PhysicsCastMotionResult3D.new()
    var total := 0
    for s in SAMPLES:
        var start := Time.get_ticks_usec()
        for i in ITERATIONS: 
            space_state.cast_motion_typed(params, result)
        var elapsed := Time.get_ticks_usec() - start
        total += elapsed
    return total / float(SAMPLES * ITERATIONS)

static func _collide_shape_time_3d(space_state: PhysicsDirectSpaceState3D, params: PhysicsShapeQueryParameters3D) -> float:    
    var total := 0
    for s in SAMPLES:
        var start := Time.get_ticks_usec()
        for i in ITERATIONS: 
            space_state.collide_shape(params)
        var elapsed := Time.get_ticks_usec() - start
        total += elapsed
    return total / float(SAMPLES * ITERATIONS)

static func _collide_shape_typed_time_3d(space_state: PhysicsDirectSpaceState3D, params: PhysicsShapeQueryParameters3D) -> float:    
    var result := PhysicsShapeCollisionResult3D.new()
    var total := 0
    for s in SAMPLES:
        var start := Time.get_ticks_usec()
        for i in ITERATIONS: 
            space_state.collide_shape_typed(params, result)
        var elapsed := Time.get_ticks_usec() - start
        total += elapsed
    return total / float(SAMPLES * ITERATIONS)

static func _get_rest_info_time_3d(space_state: PhysicsDirectSpaceState3D, params: PhysicsShapeQueryParameters3D) -> float:    
    var total := 0
    for s in SAMPLES:
        var start := Time.get_ticks_usec()
        for i in ITERATIONS: 
            space_state.get_rest_info(params)
        var elapsed := Time.get_ticks_usec() - start
        total += elapsed
    return total / float(SAMPLES * ITERATIONS)

static func _get_rest_info_typed_time_3d(space_state: PhysicsDirectSpaceState3D, params: PhysicsShapeQueryParameters3D) -> float:    
    var result := PhysicsRestInfoResult3D.new()
    var total := 0
    for s in SAMPLES:
        var start := Time.get_ticks_usec()
        for i in ITERATIONS: 
            space_state.get_rest_info_typed(params, result)
        var elapsed := Time.get_ticks_usec() - start
        total += elapsed
    return total / float(SAMPLES * ITERATIONS)

func _ready() -> void:
    var scene_tree: SceneTree = Engine.get_main_loop()

    # INFO: 2D

    var direct_space_state_2d := scene_tree.root.world_2d.direct_space_state

    var shape_2d_rid := PhysicsServer2D.circle_shape_create()
    PhysicsServer2D.shape_set_data(shape_2d_rid, 10.0)
    var body_2d_rid := PhysicsServer2D.body_create()
    PhysicsServer2D.body_set_mode(body_2d_rid, PhysicsServer2D.BODY_MODE_STATIC)
    PhysicsServer2D.body_set_space(body_2d_rid, scene_tree.root.world_2d.space)
    PhysicsServer2D.body_add_shape(body_2d_rid, shape_2d_rid)
    PhysicsServer2D.body_set_state(body_2d_rid, PhysicsServer2D.BODY_STATE_TRANSFORM, Transform2D())

    var ray_params_2d := PhysicsRayQueryParameters2D.new()
    ray_params_2d.from = 20.0 * Vector2.DOWN
    ray_params_2d.to = Vector2.ZERO

    var point_params_2d := PhysicsPointQueryParameters2D.new()
    point_params_2d.position = 5.0 * Vector2.UP

    var shape_params_2d := PhysicsShapeQueryParameters2D.new()
    shape_params_2d.shape = RectangleShape2D.new()
    shape_params_2d.shape.size = 10.0 * Vector2.ONE
    shape_params_2d.transform = Transform2D()
    shape_params_2d.transform.origin = 5.0 * Vector2.UP

    var cast_params_2d := PhysicsShapeQueryParameters2D.new()
    cast_params_2d.shape = CircleShape2D.new()
    cast_params_2d.shape.radius = 10.0
    cast_params_2d.transform = Transform2D()
    cast_params_2d.transform.origin = 40.0 * Vector2.DOWN
    cast_params_2d.motion = 50.0 * Vector2.UP

    _intersect_ray_typed_test_2d(direct_space_state_2d, ray_params_2d)
    _intersect_point_typed_test_2d(direct_space_state_2d, point_params_2d)
    _intersect_shape_typed_test_2d(direct_space_state_2d, shape_params_2d)
    _cast_motion_typed_test_2d(direct_space_state_2d, cast_params_2d)
    _collide_shape_typed_test_2d(direct_space_state_2d, shape_params_2d)
    _get_rest_info_typed_test_2d(direct_space_state_2d, shape_params_2d)

    var intersect_ray_usec_2d         := _intersect_ray_time_2d(direct_space_state_2d, ray_params_2d)
    var intersect_ray_typed_usec_2d   := _intersect_ray_typed_time_2d(direct_space_state_2d, ray_params_2d)    
    var intersect_point_usec_2d       := _intersect_point_time_2d(direct_space_state_2d, point_params_2d)
    var intersect_point_typed_usec_2d := _intersect_point_typed_time_2d(direct_space_state_2d, point_params_2d)
    var intersect_shape_usec_2d       := _intersect_shape_time_2d(direct_space_state_2d, shape_params_2d)
    var intersect_shape_typed_usec_2d := _intersect_shape_typed_time_2d(direct_space_state_2d, shape_params_2d)
    var cast_motion_usec_2d           := _cast_motion_time_2d(direct_space_state_2d, cast_params_2d)
    var cast_motion_typed_usec_2d     := _cast_motion_typed_time_2d(direct_space_state_2d, cast_params_2d)
    var collide_shape_usec_2d         := _collide_shape_time_2d(direct_space_state_2d, shape_params_2d)
    var collide_shape_typed_usec_2d   := _collide_shape_typed_time_2d(direct_space_state_2d, shape_params_2d)
    var get_rest_info_usec_2d         := _get_rest_info_time_2d(direct_space_state_2d, shape_params_2d)
    var get_rest_info_typed_usec_2d   := _get_rest_info_typed_time_2d(direct_space_state_2d, shape_params_2d)

    print("engine: %s" % ProjectSettings.get_setting("physics/2d/physics_engine"))
    print("-------------------------------------")
    print("samples: %d, iterations: %d" % [SAMPLES, ITERATIONS])
    print("-------------------------------------")
    print("intersect_ray        : %9.6f usec" % intersect_ray_usec_2d)
    print("intersect_ray_typed  : %9.6f usec" % intersect_ray_typed_usec_2d)
    print("intersect_point      : %9.6f usec" % intersect_point_usec_2d)
    print("intersect_point_typed: %9.6f usec" % intersect_point_typed_usec_2d)
    print("intersect_shape      : %9.6f usec" % intersect_shape_usec_2d)
    print("intersect_shape_typed: %9.6f usec" % intersect_shape_typed_usec_2d)
    print("cast_motion          : %9.6f usec" % cast_motion_usec_2d)
    print("cast_motion_typed    : %9.6f usec" % cast_motion_typed_usec_2d)
    print("collide_shape        : %9.6f usec" % collide_shape_usec_2d)
    print("collide_shape_typed  : %9.6f usec" % collide_shape_typed_usec_2d)
    print("get_rest_info        : %9.6f usec" % get_rest_info_usec_2d)
    print("get_rest_info_typed  : %9.6f usec" % get_rest_info_typed_usec_2d)
    print("-------------------------------------")
    print("intersect_ray_typed  : %5.2fx speedup" % (intersect_ray_usec_2d / intersect_ray_typed_usec_2d))
    print("intersect_point_typed: %5.2fx speedup" % (intersect_point_usec_2d / intersect_point_typed_usec_2d))
    print("intersect_shape_typed: %5.2fx speedup" % (intersect_shape_usec_2d / intersect_shape_typed_usec_2d))
    print("cast_motion_typed    : %5.2fx speedup" % (cast_motion_usec_2d / cast_motion_typed_usec_2d))
    print("collide_shape_typed  : %5.2fx speedup" % (collide_shape_usec_2d / collide_shape_typed_usec_2d))
    print("get_rest_info_typed  : %5.2fx speedup" % (get_rest_info_usec_2d / get_rest_info_typed_usec_2d))

    # INFO: 3D

    var direct_space_state_3d := scene_tree.root.world_3d.direct_space_state

    var shape_3d_rid := PhysicsServer3D.sphere_shape_create()
    PhysicsServer3D.shape_set_data(shape_3d_rid, 1.0)
    var body_3d_rid := PhysicsServer3D.body_create()
    PhysicsServer3D.body_set_mode(body_3d_rid, PhysicsServer3D.BODY_MODE_STATIC)
    PhysicsServer3D.body_set_space(body_3d_rid, scene_tree.root.world_3d.space)
    PhysicsServer3D.body_add_shape(body_3d_rid, shape_3d_rid)
    PhysicsServer3D.body_set_state(body_3d_rid, PhysicsServer3D.BODY_STATE_TRANSFORM, Transform3D())

    var ray_params_3d := PhysicsRayQueryParameters3D.new()
    ray_params_3d.from = 2.0 * Vector3.DOWN
    ray_params_3d.to = Vector3.ZERO

    var point_params_3d := PhysicsPointQueryParameters3D.new()
    point_params_3d.position = 0.5 * Vector3.UP

    var shape_params_3d := PhysicsShapeQueryParameters3D.new()
    shape_params_3d.shape = BoxShape3D.new()
    shape_params_3d.shape.size = Vector3.ONE
    shape_params_3d.transform = Transform3D()
    shape_params_3d.transform.origin = 0.5 * Vector3.UP

    var cast_params_3d := PhysicsShapeQueryParameters3D.new()
    cast_params_3d.shape = SphereShape3D.new()
    cast_params_3d.shape.radius = 1.0
    cast_params_3d.transform = Transform3D()
    cast_params_3d.transform.origin = 4.0 * Vector3.BACK
    cast_params_3d.motion = 5.0 * Vector3.UP

    _intersect_ray_typed_test_3d(direct_space_state_3d, ray_params_3d)
    _intersect_point_typed_test_3d(direct_space_state_3d, point_params_3d)
    _intersect_shape_typed_test_3d(direct_space_state_3d, shape_params_3d)
    _cast_motion_typed_test_3d(direct_space_state_3d, cast_params_3d)
    _collide_shape_typed_test_3d(direct_space_state_3d, shape_params_3d)
    _get_rest_info_typed_test_3d(direct_space_state_3d, shape_params_3d)

    var intersect_ray_usec_3d         := _intersect_ray_time_3d(direct_space_state_3d, ray_params_3d)
    var intersect_ray_typed_usec_3d   := _intersect_ray_typed_time_3d(direct_space_state_3d, ray_params_3d)    
    var intersect_point_usec_3d       := _intersect_point_time_3d(direct_space_state_3d, point_params_3d)
    var intersect_point_typed_usec_3d := _intersect_point_typed_time_3d(direct_space_state_3d, point_params_3d)
    var intersect_shape_usec_3d       := _intersect_shape_time_3d(direct_space_state_3d, shape_params_3d)
    var intersect_shape_typed_usec_3d := _intersect_shape_typed_time_3d(direct_space_state_3d, shape_params_3d)
    var cast_motion_usec_3d           := _cast_motion_time_3d(direct_space_state_3d, cast_params_3d)
    var cast_motion_typed_usec_3d     := _cast_motion_typed_time_3d(direct_space_state_3d, cast_params_3d)
    var collide_shape_usec_3d         := _collide_shape_time_3d(direct_space_state_3d, shape_params_3d)
    var collide_shape_typed_usec_3d   := _collide_shape_typed_time_3d(direct_space_state_3d, shape_params_3d)
    var get_rest_info_usec_3d         := _get_rest_info_time_3d(direct_space_state_3d, shape_params_3d)
    var get_rest_info_typed_usec_3d   := _get_rest_info_typed_time_3d(direct_space_state_3d, shape_params_3d)

    print("engine: %s" % ProjectSettings.get_setting("physics/3d/physics_engine"))
    print("-------------------------------------")
    print("samples: %d, iterations: %d" % [SAMPLES, ITERATIONS])
    print("-------------------------------------")
    print("intersect_ray        : %9.6f usec" % intersect_ray_usec_3d)
    print("intersect_ray_typed  : %9.6f usec" % intersect_ray_typed_usec_3d)
    print("intersect_point      : %9.6f usec" % intersect_point_usec_3d)
    print("intersect_point_typed: %9.6f usec" % intersect_point_typed_usec_3d)
    print("intersect_shape      : %9.6f usec" % intersect_shape_usec_3d)
    print("intersect_shape_typed: %9.6f usec" % intersect_shape_typed_usec_3d)
    print("cast_motion          : %9.6f usec" % cast_motion_usec_3d)
    print("cast_motion_typed    : %9.6f usec" % cast_motion_typed_usec_3d)
    print("collide_shape        : %9.6f usec" % collide_shape_usec_3d)
    print("collide_shape_typed  : %9.6f usec" % collide_shape_typed_usec_3d)
    print("get_rest_info        : %9.6f usec" % get_rest_info_usec_3d)
    print("get_rest_info_typed  : %9.6f usec" % get_rest_info_typed_usec_3d)
    print("-------------------------------------")
    print("intersect_ray_typed  : %5.2fx speedup" % (intersect_ray_usec_3d / intersect_ray_typed_usec_3d))
    print("intersect_point_typed: %5.2fx speedup" % (intersect_point_usec_3d / intersect_point_typed_usec_3d))
    print("intersect_shape_typed: %5.2fx speedup" % (intersect_shape_usec_3d / intersect_shape_typed_usec_3d))
    print("cast_motion_typed    : %5.2fx speedup" % (cast_motion_usec_3d / cast_motion_typed_usec_3d))
    print("collide_shape_typed  : %5.2fx speedup" % (collide_shape_usec_3d / collide_shape_typed_usec_3d))
    print("get_rest_info_typed  : %5.2fx speedup" % (get_rest_info_usec_3d / get_rest_info_typed_usec_3d))
engine: GodotPhysics2D
-------------------------------------
samples: 1000, iterations: 1000
-------------------------------------
intersect_ray        :  1.937766 usec
intersect_ray_typed  :  0.179960 usec
intersect_point      :  1.779098 usec
intersect_point_typed:  0.119219 usec
intersect_shape      :  2.099370 usec
intersect_shape_typed:  0.336587 usec
cast_motion          :  2.134394 usec
cast_motion_typed    :  1.945879 usec
collide_shape        :  0.836287 usec
collide_shape_typed  :  0.411583 usec
get_rest_info        :  2.379859 usec
get_rest_info_typed  :  0.422059 usec
-------------------------------------
intersect_ray_typed  : 10.77x speedup
intersect_point_typed: 14.92x speedup
intersect_shape_typed:  6.24x speedup
cast_motion_typed    :  1.10x speedup
collide_shape_typed  :  2.03x speedup
get_rest_info_typed  :  5.64x speedup
engine: GodotPhysics3D
-------------------------------------
samples: 1000, iterations: 1000
-------------------------------------
intersect_ray        :  2.383442 usec
intersect_ray_typed  :  0.230930 usec
intersect_point      :  1.961021 usec
intersect_point_typed:  0.146479 usec
intersect_shape      :  2.017160 usec
intersect_shape_typed:  0.199051 usec
cast_motion          :  0.379957 usec
cast_motion_typed    :  0.162275 usec
collide_shape        :  0.658799 usec
collide_shape_typed  :  0.229680 usec
get_rest_info        :  2.153465 usec
get_rest_info_typed  :  0.236624 usec
-------------------------------------
intersect_ray_typed  : 10.32x speedup
intersect_point_typed: 13.39x speedup
intersect_shape_typed: 10.13x speedup
cast_motion_typed    :  2.34x speedup
collide_shape_typed  :  2.87x speedup
get_rest_info_typed  :  9.10x speedup
engine: Jolt Physics
-------------------------------------
samples: 1000, iterations: 1000
-------------------------------------
intersect_ray        :  2.284354 usec
intersect_ray_typed  :  0.245786 usec
intersect_point      :  1.902627 usec
intersect_point_typed:  0.165365 usec
intersect_shape      :  2.289464 usec
intersect_shape_typed:  0.496180 usec
cast_motion          :  0.401026 usec
cast_motion_typed    :  0.187082 usec
collide_shape        :  0.914595 usec
collide_shape_typed  :  0.492969 usec
get_rest_info        :  2.287798 usec
get_rest_info_typed  :  0.464950 usec
-------------------------------------
intersect_ray_typed  :  9.29x speedup
intersect_point_typed: 11.51x speedup
intersect_shape_typed:  4.61x speedup
cast_motion_typed    :  2.14x speedup
collide_shape_typed  :  1.86x speedup
get_rest_info_typed  :  4.92x speedup

Closes

Related PRs

@aterray
aterray requested review from a team as code owners December 12, 2025 22:56
@aterray
aterray force-pushed the feature/directspace-typed-results branch from a8367c9 to bdbead3 Compare December 12, 2025 23:01
@aterray
aterray requested review from a team as code owners December 12, 2025 23:01
@aterray
aterray force-pushed the feature/directspace-typed-results branch 2 times, most recently from 5d1900b to e3d2154 Compare December 12, 2025 23:45
@AThousandShips AThousandShips added this to the 4.x milestone Dec 15, 2025
@aterray
aterray force-pushed the feature/directspace-typed-results branch 2 times, most recently from 747916a to bd00fab Compare December 15, 2025 10:47
@AThousandShips

Copy link
Copy Markdown
Member

Thank you for your contribution!

This is a pretty majorly breaking change, is there a proposal to track this or to evaluate support?

This would benefit from having a proper proposal created here to discuss the details and evaluate support for this, as it would be something that would need good support to justify the changes and the need for them

I'd say backwards compatibility needs to be maintained by creating new methods instead of replacing the existing ones, as this would be very disruptive to anyone using the existing interfaces

@aterray
aterray force-pushed the feature/directspace-typed-results branch 7 times, most recently from 0481c49 to 1b9423b Compare December 19, 2025 07:18
@Bromeon

Bromeon commented Dec 19, 2025

Copy link
Copy Markdown
Contributor

This is a very valuable pull request, however I share @AThousandShips's concerns about compatibility breakage. This would not only create extra work for users when updating Godot versions, but it would make it very difficult to write plugins that support multiple engine versions simultaneously.

To align on the direction, it would be great if you could respond to the high-level concerns before changing implementation details. Please also note that every push notifies 4 reviewer teams. Thanks! 🙂

@aterray aterray closed this Dec 19, 2025
@aterray

aterray commented Dec 19, 2025

Copy link
Copy Markdown
Author

Thank you for your contribution!

This is a pretty majorly breaking change, is there a proposal to track this or to evaluate support?

This would benefit from having a proper proposal created here to discuss the details and evaluate support for this, as it would be something that would need good support to justify the changes and the need for them

I'd say backwards compatibility needs to be maintained by creating new methods instead of replacing the existing ones, as this would be very disruptive to anyone using the existing interfaces

This is a very valuable pull request, however I share @AThousandShips's concerns about compatibility breakage. This would not only create extra work for users when updating Godot versions, but it would make it very difficult to write plugins that support multiple engine versions simultaneously.

To align on the direction, it would be great if you could respond to the high-level concerns before changing implementation details. Please also note that every push notifies 4 reviewer teams. Thanks! 🙂

Thanks for the feedback.

I agree this is a significant breaking change, and I understand the concerns around backward compatibility and plugin support. This PR was intended to demonstrate a redesigned API focused on performance and type safety, not to incrementally extend the existing one while keeping compatibility.

Since this direction would need a proposal and broader consensus first, and maintaining parallel APIs is outside what I can commit to right now, I will close this PR in its current form. If anyone wants to follow up with a proposal or continue the discussion, it can always be reopened.

Apologies for spamming the notifications with all the pushes 😅

@AThousandShips AThousandShips added this to the 4.x milestone Jul 19, 2026
@aterray aterray closed this Jul 19, 2026
@aterray
aterray deleted the feature/directspace-typed-results branch July 19, 2026 22:08
@aterray
aterray restored the feature/directspace-typed-results branch July 19, 2026 22:09
@aterray aterray reopened this Jul 19, 2026
@aterray aterray closed this Jul 19, 2026
@aterray
aterray deleted the feature/directspace-typed-results branch July 19, 2026 22:12
@aterray
aterray restored the feature/directspace-typed-results branch July 19, 2026 22:13
@aterray aterray reopened this Jul 19, 2026
@aterray
aterray force-pushed the feature/directspace-typed-results branch from 893a7d1 to 1fc32ac Compare July 19, 2026 23:49
@Chestnut45

Copy link
Copy Markdown
Contributor

I want to put my 2 cents in now that I've had a chance to read over the new implementation. This is the perfect solution. I understand and agree that it's important to vet large PRs, especially from new contributors, but please don't let a gem like this fall through the cracks without a careful review first just because it's a feature PR (It's arguable that this is even a "new" feature, since much of the current API was only meant to be a temporary solution until a better method was found).

Under the guidelines I believe I'm also considered a new contributor, but I do have a degree in computer science with a specialization in video game programming, and would be more than happy to contribute to any formalized review process if the motivation isn't there for current maintainers.

@Mickeon

Mickeon commented Jul 20, 2026

Copy link
Copy Markdown
Member

It may be ideal to encourage discussions on this PR on RocketChat, possibly in the #physics channel.

@aterray aterray changed the title PhysicsDirectSpaceState2D/3D: reusable strongly typed results PhysicsDirectSpaceState2D/3D: add typed result objects Jul 21, 2026
@aterray
aterray force-pushed the feature/directspace-typed-results branch from 1fc32ac to 2c63f83 Compare July 28, 2026 02:28
@aterray
aterray force-pushed the feature/directspace-typed-results branch from 2c63f83 to c8ad4e8 Compare July 28, 2026 03:19
@aterray

aterray commented Jul 28, 2026

Copy link
Copy Markdown
Author

I wonder if the .collision_count and .max_collisions properties of PhysicsPointIntersectionResult2D/3D and PhysicsShapeIntersectionResult2D/3D should instead be named .intersection_count and .max_intersections. Likewise, the collision_index parameter of their methods could be renamed to intersection_index.

Initially, I chose these names for consistency with PhysicsShapeCollisionResult2D/3D, but I now think consistency should instead follow the methods that produce these results intersect_point_typed() and intersect_shape_typed()

@Chestnut45

Copy link
Copy Markdown
Contributor

I think the internal consistency of the new result types' properties following the methods for naming like that is a good idea and is unambiguous, but it may be simpler (and more in line with the current API) to use max_results, result_count, and result_index for all of them, to mirror what the current query methods use for wording.

I have no strong feelings either way though, and think both are perfectly acceptable. Whatever is preferred by maintainers :)

@Chestnut45

Copy link
Copy Markdown
Contributor

Linking the small discussion that was had in RocketChat here so it's easier to find for reviewers: https://chat.godotengine.org/channel/physics/thread/6WEXTx24Rdt6LijTv

Towards the end of the thread I explain why structs / custom variants are necessarily worse solutions, since they would still require per-query allocations even if we had those features today, unlike this PR.

As well, please don't be put off by the +2,000 lines. It's mostly just documentation and class boilerplate for the new result types and methods, and there isn't even any new logic to review.

This should be a slam dunk; It benefits all users, there's no downsides, and it solves the problem at the source without adding any complexity.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

5 participants