Skip to content

Commit

Permalink
Add cross-platform determinism (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
lokimckay authored May 27, 2024
1 parent 9a7b939 commit ad6a85f
Show file tree
Hide file tree
Showing 44 changed files with 2,403 additions and 1,184 deletions.
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"rust-analyzer.linkedProjects": ["./addons/godot-rapier-3d/rust/Cargo.toml"]
}
17 changes: 1 addition & 16 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

- Follow [semver](https://semver.org/) when releasing new versions
- If functionality doesn't need to directly interact with Rapier and doesn't need to be optimized, prefer writing it in GDScript
- Any mutations to the Rapier pipeline need to be fed through the action queue in order to ensure determinism

## Development quickstart

Expand All @@ -25,22 +26,6 @@

Please raise an issue and provide reproducible steps or a minimal reproduction project, which is a small Godot project which reproduces the issue, with no unnecessary files included.

## Known issues

- Using any of the logging macros (`crate::error!` etc.) grabs a mutable reference to the engine singleton. If you already have done this in the same function, Godot will crash. You instead need to pass the engine bind:

```rust
fn my_func() {
let mut engine = crate::get_engine!();
let mut bind = engine.bind_mut();
// ...stuff...
// crate::error!("This will crash Godot"); // INCORRECT
crate::error!(bind; "This is fine");
}
```

the `get_engine!` macro should be upgraded to prevent such double access cases

## Roadmap

- [x] Visualize colliders
Expand Down
15 changes: 1 addition & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ It is _not_ a drop-in replacement for the Godot physics engine. Rapier nodes ope

### Features

- Cross platform determinism (TBC - needs testing)
- Cross platform determinism (confirmed via [actions](https://github.com/deltasiege/godot-rapier-3d/actions/workflows/build-and-test.yml)!)
- Physics state manual stepping
- Physics state saving & loading

Expand Down Expand Up @@ -54,22 +54,13 @@ Obtain a [hash](https://docs.godotengine.org/en/stable/classes/class_array.html#
var initial_snapshot
func _ready():
Rapier3D.physics_ready.connect(_on_physics_ready)
func _on_physics_ready():
initial_snapshot = Rapier3D.get_state()
var hash = Rapier3D.get_hash(initial_snapshot)
func _on_foo():
Rapier3D.set_state(initial_snapshot)
```

### Why `_on_physics_ready`?

Colliders need 1 extra frame to attach to physics bodies. Therefore it's recommended that you don't use `Rapier3D.get_state()` within a `_ready()` function because colliders will not be attached yet.

Instead, you should connect to the `Rapier3D.physics_ready` signal as shown above.

## Why does this exist?

Currently Godot does not support [on-demand physics simulation](https://github.com/godotengine/godot-proposals/issues/2821), does not have [built-in snapshotting](https://github.com/godotengine/godot-proposals/issues/7041), and is also not [deterministic](https://gafferongames.com/post/deterministic_lockstep).
Expand All @@ -82,10 +73,6 @@ Luckily, Godot 4 provides a great [extension system][gdext-link] and [Rapier][ra

- No mobile support ([godot-rust](https://github.com/godot-rust/gdext/issues/24))

## Known issues

- `Project -> Reload current project` can cause the engine to not load properly - errors will print to the godot console. These errors can be safely ignored. To eliminate them, close godot entirely and reopen your project from the project manager.

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md)
Expand Down
7 changes: 4 additions & 3 deletions addons/godot-rapier-3d/Rapier3D.gd
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
@tool
extends Node3D

const utils = preload("res://addons/godot-rapier-3d/gdscript/utils.gd")
const physics_state = preload("res://addons/godot-rapier-3d/gdscript/physics_state.gd")

signal physics_ready
func _ready():
Rapier3DEngine._process()

func _physics_process(_delta):
if Engine.get_physics_frames() != 1: return # Need to wait a few frames for colliders to properly mount in the tree
physics_ready.emit()
Rapier3DEngine._process()

func step() -> void:
Rapier3DEngine.step()
Expand Down
7 changes: 6 additions & 1 deletion addons/godot-rapier-3d/Rapier3DDebugger.gd
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,9 @@ func _draw_line(a, b, color):

func _should_run():
if Engine.is_editor_hint(): return run_in_editor
else: return run_in_game
else:
var current_scene = get_tree().current_scene
var scene_path = current_scene.scene_file_path
var is_test_scene = scene_path.contains("res://tests/")
if is_test_scene: return false
return run_in_game
5 changes: 5 additions & 0 deletions addons/godot-rapier-3d/debug_info.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@tool
extends EditorScript

func _run():
Rapier3DEngine.print_debug_info()
5 changes: 1 addition & 4 deletions addons/godot-rapier-3d/gdscript/physics_controls.gd
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,11 @@ var godot_hash
func _ready():
_update_should_show()
if !should_show: return
Rapier3D.physics_ready.connect(_on_physics_ready)
initial_snapshot = _save()
if !Engine.is_editor_hint():
play_button.set_pressed(true)
play = true

func _on_physics_ready():
initial_snapshot = _save()

func _physics_process(_delta):
if !should_show: return
if play: Rapier3D.step()
Expand Down
2 changes: 1 addition & 1 deletion addons/godot-rapier-3d/gdscript/physics_state.gd
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const utils = preload("res://addons/godot-rapier-3d/gdscript/utils.gd")

static func _get_physics_state(root: Node3D):
static func _get_physics_state(root: Node):
var state = PackedByteArray()
var physics_objects = _get_all_physics_objects(root)
var sorted = _sort_by_iid(physics_objects)
Expand Down
7 changes: 4 additions & 3 deletions addons/godot-rapier-3d/gdscript/project_settings.gd
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ extends Object
var _property_order: int = 1000

var _project_settings = [
{ "name": "debug/rapier_3d/logging_level", "type": TYPE_INT, "default": 2, "hint": PROPERTY_HINT_ENUM, "hint_string": "Error,Warning,Info,Debug" },
{ "name": "debug/rapier_3d/logging_level", "type": TYPE_STRING, "default": "Info", "hint": PROPERTY_HINT_ENUM, "hint_string": "Off,Error,Warning,Info,Debug,Trace" },
{ "name": "debug/rapier_3d/debug_in_game", "type": TYPE_BOOL, "default": true },
{ "name": "debug/rapier_3d/debug_in_editor", "type": TYPE_BOOL, "default": true },
{ "name": "debug/rapier_3d/show_colliders", "type": TYPE_BOOL, "default": true },
{ "name": "debug/rapier_3d/show_ui", "type": TYPE_BOOL, "default": true },
{ "name": "physics/rapier_3d/gravity_vector", "type": TYPE_VECTOR3, "default": Vector3(0, -9.8, 0) },
]

func _add_project_setting(name: String, type: int, default, hint = null, hint_string = null) -> void:
func _add_project_setting(name: String, type: int, default, hint = null, hint_string = null, restart_if_changed = false) -> void:
if not ProjectSettings.has_setting(name): ProjectSettings.set_setting(name, default)
ProjectSettings.set_initial_value(name, default)
ProjectSettings.set_order(name, _property_order)
Expand All @@ -20,13 +20,14 @@ func _add_project_setting(name: String, type: int, default, hint = null, hint_st
if hint != null: info['hint'] = hint
if hint_string != null: info['hint_string'] = hint_string
ProjectSettings.add_property_info(info)
ProjectSettings.set_restart_if_changed(name, restart_if_changed)

func _remove_project_setting(name: String) -> void:
if ProjectSettings.has_setting(name): ProjectSettings.set_setting(name, null)

func add_project_settings() -> void:
for setting in _project_settings:
_add_project_setting(setting.name, setting.type, setting.default, setting.get("hint", null), setting.get("hint_string", null))
_add_project_setting(setting.name, setting.type, setting.default, setting.get("hint", null), setting.get("hint_string", null), setting.get("restart_if_changed", false))

func remove_project_settings() -> void:
for setting in _project_settings:
Expand Down
Loading

0 comments on commit ad6a85f

Please sign in to comment.