diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..38adf3c --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,22 @@ +Checks: + - bugprone-* + - -bugprone-easily-swappable-parameters + - cert-* + - clang-analyzer-* + - concurrency-* + - cppcoreguidelines-* + - -cppcoreguidelines-init-variables # Produces false positves when initialization happens later + - -cppcoreguidelines-avoid-magic-numbers # This also triggers with literals like 3.1415 + - -cppcoreguidelines-avoid-non-const-global-variables + - misc-* + - -misc-non-private-member-variables-in-classes + - -misc-use-anonymous-namespace + - -misc-include-cleaner + - performance-* + - portability-* + - readability-* + - -readability-braces-around-statements + - -readability-uppercase-literal-suffix + - -readability-magic-numbers # This also triggers with literals like 3.1415 + - -readability-redundant-access-specifiers + - -readability-implicit-bool-conversion diff --git a/.clangd b/.clangd new file mode 100644 index 0000000..5a76471 --- /dev/null +++ b/.clangd @@ -0,0 +1,6 @@ +CompileFlags: + Remove: + - -fno-gnu-unique + Add: + - -Wall + - -Wextra diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c0e5476..0d111a7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,97 +1,67 @@ # Adapted from https://github.com/nathanfranke/gdextension/blob/main/.github/workflows/build.yml name: Builds on: - push: - branches: [ "master" ] - pull_request: - branches: [ "master" ] + workflow_dispatch: + inputs: + git-ref: + description: A commit, branch or tag to build. + type: string + required: true + workflow_call: + inputs: + git-ref: + description: A commit, branch or tag to build. + type: string + required: true jobs: build: runs-on: ${{ matrix.runner }} - name: ${{ matrix.name }} + name: ${{ matrix.platform }} ${{ matrix.target }} ${{ matrix.arch }} ${{ matrix.optimize }} strategy: fail-fast: false matrix: + target: [ template_debug, template_release ] + identifier: [ windows, linux, macos, android, android_arm64 ] + include: - # Linux - - identifier: linux-debug - name: Linux Debug - runner: ubuntu-20.04 - target: template_debug - platform: linux - arch: x86_64 + # Defaults + - runner: ubuntu-latest + - optimize: speed + - arch: x86_64 - - identifier: linux-release - name: Linux Release - runner: ubuntu-20.04 - target: template_release - platform: linux - arch: x86_64 + # Debug build settings + - target: template_debug + optimize: speed_trace - # Windows - - identifier: windows-debug - name: Windows Debug - runner: ubuntu-20.04 - target: template_debug + # Map identifiers to platforms + special settings + - identifier: windows platform: windows - arch: x86_64 - - identifier: windows-release - name: Windows Release - runner: ubuntu-20.04 - target: template_release - platform: windows - arch: x86_64 - - # Android Arm64 - - identifier: android-release - name: Android Release Arm64 - runner: ubuntu-20.04 - target: template_release - platform: android - arch: arm64 + - identifier: macos + platform: macos + runner: macos-latest + arch: universal - - identifier: android-debug - name: Android Debug Arm64 - runner: ubuntu-20.04 - target: template_debug - platform: android - arch: arm64 + - identifier: linux + platform: linux - # Android x86_64 - - identifier: android-release - name: Android Release x86_64 - runner: ubuntu-20.04 - target: template_release + - identifier: android platform: android - arch: x86_64 - - identifier: android-debug - name: Android Debug x86_64 - runner: ubuntu-20.04 - target: template_debug + - identifier: android_arm64 platform: android - arch: x86_64 - - # Mac - - identifier: macos-debug - name: macOS (universal) Debug - runner: macos-latest - target: template_debug - platform: macos - arch: universal - - - identifier: macos-release - name: macOS (universal) Release - runner: macos-latest - target: template_release - platform: macos - arch: universal + arch: arm64 steps: + - name: Check settings + if: ${{ matrix.platform == '' || matrix.target == '' || matrix.runner == '' || matrix.optimize == '' || matrix.arch == ''}} + run: | + echo "One of the matrix values is not set." + exit 1 + - name: (Windows) Install mingw64 - if: ${{ startsWith(matrix.identifier, 'windows-') }} + if: ${{ matrix.platform == 'windows' }} shell: sh run: | sudo apt-get install mingw-w64 @@ -99,19 +69,19 @@ jobs: sudo update-alternatives --set x86_64-w64-mingw32-g++ /usr/bin/x86_64-w64-mingw32-g++-posix - name: (Android) Install JDK 17 - if: ${{ startsWith(matrix.identifier, 'android-') }} + if: ${{ matrix.platform == 'android' }} uses: actions/setup-java@v3 with: java-version: 17 distribution: temurin - name: (Android) Install Android SDK - if: ${{ startsWith(matrix.identifier, 'android-') }} + if: ${{ matrix.platform == 'android' }} uses: android-actions/setup-android@v3 # From Godot docs, might not be necessary. #- name: (Android) Install Android Tools - # if: ${{ startsWith(matrix.identifier, 'android-') }} + # if: ${{ matrix.platform == 'android' }} # shell: sh # run: | # "$ANDROID_SDK_ROOT"/cmdline-tools/latest/bin/sdkmanager --sdk_root="$ANDROID_SDK_ROOT" "platform-tools" "build-tools;30.0.3" "platforms;android-29" "cmdline-tools;latest" "cmake;3.10.2.4988404" @@ -124,19 +94,22 @@ jobs: link-to-sdk: true - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 + with: + python-version: '3.10' - name: Set up SCons shell: bash run: | python -c "import sys; print(sys.version)" - python -m pip install scons + python -m pip install scons==4.7.0 scons --version - name: Checkout project - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: submodules: recursive + ref: ${{ inputs.git-ref }} # TODO: Cache doesn't work yet. SCons rebuilds the objects even if they already exist. Could be caused by modification dates or extension_api.json. # fetch-depth: 0 May be needed for cache. See: . @@ -147,11 +120,11 @@ jobs: # ${{ github.workspace }}/.scons-cache/ # ${{ github.workspace }}/**/.sconsign.dblite # ${{ github.workspace }}/godot-cpp/gen/ -# key: ${{ matrix.identifier }}-${{ github.ref }}-${{ github.sha }} +# key: ${{ matrix.platform }}-${{ github.ref }}-${{ github.sha }} # restore-keys: | -# ${{ matrix.identifier }}-${{ github.ref }}-${{ github.sha }} -# ${{ matrix.identifier }}-${{ github.ref }} -# ${{ matrix.identifier }} +# ${{ matrix.platform }}-${{ github.ref }}-${{ github.sha }} +# ${{ matrix.platform }}-${{ github.ref }} +# ${{ matrix.platform }} - name: Compile extension shell: sh @@ -159,18 +132,18 @@ jobs: # SCONS_CACHE: '${{ github.workspace }}/.scons-cache/' # SCONS_CACHE_LIMIT: 8192 run: | - scons target='${{ matrix.target }}' platform='${{ matrix.platform }}' arch='${{ matrix.arch }}' -j2 + scons target='${{ matrix.target }}' platform='${{ matrix.platform }}' arch='${{ matrix.arch }}' optimize=${{ matrix.optimize }} -j2 ls -l demo/addons/*/bin/ - - name: Copy extra files to addon + - name: Prepare files for publish shell: sh run: | - for addon in ${{ github.workspace }}/demo/addons/*/; do - cp -n '${{ github.workspace }}/README.md' '${{ github.workspace }}/LICENSE' "$addon" - done + cp -n '${{ github.workspace }}/README.md' '${{ github.workspace }}/LICENSE' '${{ github.workspace }}/demo/addons/ropesim/' + rm -rf '${{ github.workspace }}/demo/addons/diagnosticlist' + rm '${{ github.workspace }}/demo/project.godot' - name: Upload artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: ${{ github.event.repository.name }} path: | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c4487fa --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,14 @@ +name: CI +on: + push: + # branches: [ master ] + pull_request: + # branches: [ master ] + +jobs: + ci: + name: "CI" + uses: ./.github/workflows/build.yml + with: + git-ref: ${{ github.ref }} + diff --git a/README.md b/README.md index 170e488..a39b7c5 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ -A 2D verlet integration based rope simulation for Godot 4.2. +A 2D verlet integration based rope simulation for Godot 4.2+. The computation-heavy simulation part is written in C++ using GDExtension, the rest in GDScript. This allows for fast processing and easy extendability, while keeping the code readable. @@ -15,9 +15,9 @@ The last Godot 3.x version can be found on the [3.x branch](https://github.com/m * [Download](https://github.com/mphe/GDNative-Ropesim/releases/latest) the latest release from the release page, or * [Download](https://github.com/mphe/GDNative-Ropesim/actions) it from the latest GitHub Actions run, or * [Compile](#building) it yourself. -3. Copy or symlink `addons/ropesim` to your project's `addons/` directory -4. Enable the addon in the project settings -5. Restart Godot +2. Copy or symlink `addons/ropesim` to your project's `addons/` directory +3. Enable the addon in the project settings +4. Restart Godot # Building @@ -29,7 +29,7 @@ To compile for Linux, run the following commands. Compiling for other platforms works analogously. ```sh -$ scons target=template_release platform=linux arch=x86_64 -j8 +$ scons target=template_release platform=linux optimize=speed arch=x86_64 -j8 $ scons target=template_debug platform=linux arch=x86_64 -j8 ``` @@ -43,15 +43,17 @@ Following nodes exist: * `RopeHandle`: A handle that can be used to control, animate, or fixate parts of the rope. * `RopeRendererLine2D`: Renders a target rope using `Line2D`. * `RopeCollisionShapeGenerator`: Can be used e.g. in an `Area2D` to detect collisions with the target rope. - -See inline comments for further information and documentation of node properties. - -The included demo project and the showcase video below provide some usage examples. +* `RopeInteraction`: Handles mutual interaction of a target node with a rope. Useful for rope grabbing or pulling mechanics where an object should be able to affect the rope and vice-versa. When one of these nodes is selected, a "Ropesim" menu appears in the editor toolbar that can be used to toggle live preview in the editor on and off. All rope related tools, automatically pause themselves when their target rope is paused to save performance. +Use the in-engine help to view the full documentation for each node. + +The project also includes various example scenes that demonstrate the features of this plugin. +See also the [showcase video](#showcase) for a basic usage example. + # Showcase A quick overview of how to use each node. diff --git a/SConstruct b/SConstruct index 5fdf52c..8d4d384 100644 --- a/SConstruct +++ b/SConstruct @@ -26,6 +26,8 @@ opts.Update(env) # - CPPDEFINES are for pre-processor defines # - LINKFLAGS are for linking flags +env.Append(CCFLAGS="-fdiagnostics-color") + # Sources env.Append(CPPPATH=["src/"]) diff --git a/compile_debug.sh b/compile_debug.sh index ea1254d..f1572cd 100755 --- a/compile_debug.sh +++ b/compile_debug.sh @@ -1,3 +1,3 @@ #!/usr/bin/env bash -scons compiledb=yes optimize=debug +scons compiledb=yes optimize=debug use_llvm=yes diff --git a/compile_release.sh b/compile_release.sh new file mode 100755 index 0000000..718d3c4 --- /dev/null +++ b/compile_release.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +scons optimize=speed target=template_release diff --git a/demo/addons/diagnosticlist/.diagnostic_ignore b/demo/addons/diagnosticlist/.diagnostic_ignore new file mode 100644 index 0000000..e69de29 diff --git a/demo/addons/diagnosticlist/Diagnostic.gd b/demo/addons/diagnosticlist/Diagnostic.gd new file mode 100644 index 0000000..140b239 --- /dev/null +++ b/demo/addons/diagnosticlist/Diagnostic.gd @@ -0,0 +1,34 @@ +extends RefCounted +class_name DiagnosticList_Diagnostic + + +class Pack extends RefCounted: + var res_uri: StringName + var diagnostics: Array[DiagnosticList_Diagnostic] + + +enum Severity { + Error, + Warning, + Info, + Hint, +} + + +## Represents the file path as res:// path +@export var res_uri: StringName +@export var line_start: int # zero-based +@export var column_start: int # zero-based +@export var severity: Severity +@export var message: String + +var _filename: StringName + + +## Returns the filename, i.e. the last component of the path including the extension. +func get_filename() -> StringName: + if _filename.is_empty(): + _filename = StringName(res_uri.get_file()) + return _filename + + diff --git a/demo/addons/diagnosticlist/DiagnosticProvider.gd b/demo/addons/diagnosticlist/DiagnosticProvider.gd new file mode 100644 index 0000000..00bb2d3 --- /dev/null +++ b/demo/addons/diagnosticlist/DiagnosticProvider.gd @@ -0,0 +1,280 @@ +extends RefCounted +class_name DiagnosticList_DiagnosticProvider + +const IGNORE_FILES: Array[String] = [ + ".gdignore", + ".diagnostic_ignore", +] + +## Triggered when new diagnostics for a file arrived. +signal on_publish_diagnostics(diagnostics: DiagnosticList_Diagnostic.Pack) + +## Triggered when all outstanding diagnostics have been received. +signal on_diagnostics_finished + +## Triggered when sources have changed and a diagnostic update is available. +signal on_diagnostics_available + +## Triggered at the same time as on_publish_diagnostics but provides status information +signal on_update_progress(num_remaining: int, num_all: int) + + +class FileCache extends RefCounted: + var content: String = "" + var last_modified: int = -1 + + +var _diagnostics: Array[DiagnosticList_Diagnostic] = [] +var _client: DiagnosticList_LSPClient +var _script_paths: Array[String] = [] +var _counts: Array[int] = [ 0, 0, 0, 0 ] +var _num_outstanding: int = 0 +var _dirty: bool = true +var _refresh_time: int = 0 +var _file_cache := {} # Dict[String, FileCache] +var _additional_ignore_dirs: Array[String] = [] + + +func _init(client: DiagnosticList_LSPClient) -> void: + _client = client + _client.on_publish_diagnostics.connect(_on_publish_diagnostics) + _client.on_jsonrpc_error.connect(_on_jsonrpc_error) + + if Engine.is_editor_hint(): + var fs := EditorInterface.get_resource_filesystem() + + # Triggered when saving, removing and moving files. + # Also triggers whenever the user is typing or saving in an external editor using LSP. + fs.script_classes_updated.connect(_on_script_classes_updated) + + # Triggered when the Godot window receives focus and when moving or deleting files + fs.sources_changed.connect(_on_sources_changed) + + +func is_updating() -> bool: + return _num_outstanding > 0 + + +func set_additional_ignore_dirs(dirs: Array[String]) -> void: + _additional_ignore_dirs = dirs + + +## Refresh diagnostics for all scripts. +## Returns true on success or false when there are no updates available or when another update is +## still in progress. +func refresh_diagnostics(force: bool = false) -> bool: + # NOTE: We always have to do a full update, because a change in one file can cause errors in + # other files, e.g. renaming an identifier. + + # Still waiting for results from the last call + if _num_outstanding > 0: + _dirty = false # Dirty will be reset anyway after update has been finished + return false + + # Nothing changed -> Nothing to do + if not force and not _dirty: + return false + + var files_modified := refresh_file_list() + + # No files have actually been modified -> Nothing to do + if not force and not files_modified: + _dirty = false + return false + + _diagnostics.clear() + _counts = [ 0, 0, 0, 0 ] + _num_outstanding = len(_script_paths) + _refresh_time = Time.get_ticks_usec() + + if _num_outstanding > 0: + for file in _script_paths: + _client.update_diagnostics(file, _file_cache[file].content) + else: + call_deferred("_finish_update") + + # NOTE: Do not reset _dirty here, because it will be resetted anyway in _finish_update() after + # all diagnostics have been received. + return true + + +## Rescan the project for script files +## Returns true when there have been changes, otherwise false. +func refresh_file_list() -> bool: + var ignore_dirs: Array[String] = [] + ignore_dirs.assign(_additional_ignore_dirs.duplicate()) + + if ProjectSettings.get("debug/gdscript/warnings/exclude_addons"): + ignore_dirs.push_back("res://addons" ) + + _script_paths = _gather_scripts("res://", ignore_dirs) + + var modified: bool = false + + # Update cache + for path in _script_paths: + var cache: FileCache = _file_cache.get(path) + var last_modified: int = FileAccess.get_modified_time(path) + + if not cache: + cache = FileCache.new() + _file_cache[path] = cache + # The next condition will also inevitably be true + + if cache.last_modified != last_modified: + cache.last_modified = last_modified + cache.content = FileAccess.get_file_as_string(path) + modified = true + + # One or more files were deleted + if _file_cache.size() > _script_paths.size(): + modified = true + + # TODO: Could be more efficient, but happens not so often + for path: String in _file_cache.keys(): + if not _script_paths.has(path): + _file_cache.erase(path) + + return modified + + +## Get the amount of diagnostics of a given severity. +func get_diagnostic_count(severity: DiagnosticList_Diagnostic.Severity) -> int: + return _counts[severity] + + +## Returns all diagnostics of the project +func get_diagnostics() -> Array[DiagnosticList_Diagnostic]: + return _diagnostics.duplicate() + + +## Returns the amount of microseconds between requesting the last diagnostic update and the last +## diagnostic being delivered. +func get_refresh_time_usec() -> int: + return _refresh_time + + +func are_diagnostics_available() -> bool: + return _dirty + + +func get_lsp_client() -> DiagnosticList_LSPClient: + return _client + + +func _finish_update() -> void: + # NOTE: When parsing scripts using LSP, the script_classes_updated signal will be fired multiple + # times by the engine without any actual changes. + # Hence, to prevent false positive dirty flags, reset _dirty back to false when the diagnsotic + # update is finished. + # FIXME: It might happen that the user makes a change while diagnostics are still refreshing, + # In this case, the dirty flag would still be resetted, even though it shouldn't. + # This is essentially a tradeoff between efficiency and accuracy. + # As I find this exact scenario unlikely to occur regularily, I prefer the more efficient + # implementation of updating less often. + _dirty = false + + _refresh_time = Time.get_ticks_usec() - _refresh_time + on_diagnostics_finished.emit() + + +func _mark_dirty() -> void: + if not _dirty: + # If an update is currently in progress, don't do anything. _dirty will be reset anyway in + # _finish_update(). + if _num_outstanding > 0: + return + + _dirty = true + on_diagnostics_available.emit() + + +func _on_sources_changed(_exist: bool) -> void: + _mark_dirty() + + +func _on_script_classes_updated() -> void: + # NOTE: When using an external editor over LSP, the engine will constantly emit the + # script_classes_updated signal whenever the user is typing. + # In those cases it is useless to perform an update, as nothing actually changed. + # We also cannot safely determine when the user has saved a file except by comparing file + # modification timestamps. + # + # However, whenever the Godot window receives focus, a sources_changed signal is fired. + # + # Hence, to prevent unnecessary amounts of updates, check whether the Godot window has focus and + # if it doesn't, ignore the signal, as the user is likely typing in an external editor. + # + # When using the internal editor, script_classes_updated will only be fired upon saving. + # Hence, when the signal arrives and the Godot window has focus, an update should be performed. + if EditorInterface.get_base_control().get_window().has_focus(): + _mark_dirty() + + +func _on_publish_diagnostics(diagnostics: DiagnosticList_Diagnostic.Pack) -> void: + # Ignore unexpected diagnostic updates + if _num_outstanding == 0: + _client.log_error("Received diagnostics without having them requested before") + return + + _diagnostics.append_array(diagnostics.diagnostics) + + # Increase new diagnostic counts + for diag in diagnostics.diagnostics: + _counts[diag.severity] += 1 + + on_publish_diagnostics.emit(diagnostics) + + _update_outstanding_counter() + + +func _on_jsonrpc_error(_error: Dictionary) -> void: + # In case of error, it is likely something failed for a specific file. + # To prevent the plugin from effectively freezing by waiting forever for results that will never + # arrive, just update the counter as if diagnostics arrived. + if _num_outstanding > 0: + _update_outstanding_counter() + + +func _update_outstanding_counter() -> void: + _num_outstanding -= 1 + on_update_progress.emit(_num_outstanding, len(_script_paths)) + + if _num_outstanding == 0: + _finish_update() + + +# TODO: Consider making ignore_dirs a set if there will ever be more than one entry +func _gather_scripts(searchpath: String, ignore_dirs: Array[String]) -> Array[String]: + var root := DirAccess.open(searchpath) + + if not root: + push_error("Failed to open directory: ", searchpath) + + var paths: Array[String] = [] + + for ignore_file in IGNORE_FILES: + if root.file_exists(ignore_file): + return paths + + root.include_navigational = false + root.list_dir_begin() + + var fname := root.get_next() + + var root_path := root.get_current_dir() + + while not fname.is_empty(): + var path := root_path.path_join(fname) + + if root.current_is_dir(): + if not ignore_dirs.has(path): + paths.append_array(_gather_scripts(path, ignore_dirs)) + elif fname.ends_with(".gd"): + paths.append(path) + + fname = root.get_next() + + root.list_dir_end() + + return paths diff --git a/demo/addons/diagnosticlist/LSPClient.gd b/demo/addons/diagnosticlist/LSPClient.gd new file mode 100644 index 0000000..c199542 --- /dev/null +++ b/demo/addons/diagnosticlist/LSPClient.gd @@ -0,0 +1,307 @@ +extends RefCounted +class_name DiagnosticList_LSPClient + +## Triggered when connected to the LS. +signal on_connected + +## Triggered when LSP has been initialized +signal on_initialized + +## Triggered when new diagnostics for a file arrived. +signal on_publish_diagnostics(diagnostics: DiagnosticList_Diagnostic.Pack) + +## Triggered when the LSP server returns an unexpected JSON-RPC error +signal on_jsonrpc_error(error: Dictionary) + + +const ENABLE_DEBUG_LOG: bool = false +const TICK_INTERVAL_SECONDS_MIN: float = 0.05 +const TICK_INTERVAL_SECONDS_MAX: float = 30.0 + +# Godot LS expects a leading "/" in URIs. +# On Windows, where absolute paths start with C:, it must be added manually. +var URI_PREFIX := "file:///" if OS.get_name() == "Windows" else "file://" + +var _jsonrpc := JSONRPC.new() +var _client := StreamPeerTCP.new() +var _id: int = 0 +var _timer: Timer +var _lsp_project_path: String = "" # Absolute project path reported by LS + + +func _init(root: Node) -> void: + # NOTE: Since this is a RefCounted, it does not have access to the tree, hence plugin.gd passes + # the plugin root node. + _timer = Timer.new() + _timer.wait_time = TICK_INTERVAL_SECONDS_MIN + _timer.autostart = false + _timer.one_shot = false + _timer.timeout.connect(_on_tick) + root.add_child(_timer) + + +func disconnect_lsp() -> void: + log_debug("Disconnecting from LSP") + _timer.stop() + _client.disconnect_from_host() + + +## Connect to the LSP server using host and port specified in the editor config. +func connect_lsp() -> bool: + var settings := EditorInterface.get_editor_settings() + var port: int = settings.get("network/language_server/remote_port") + var host: String = settings.get("network/language_server/remote_host") + return connect_lsp_at(host, port) + + +## Connect to the LSP server at the given host and port. +func connect_lsp_at(host: String, port: int) -> bool: + var err := _client.connect_to_host(host, port) + + if err != OK: + log_error("Failed to connect to LSP server: %s" % err) + return false + + # Enable processing + _id = 0 + _timer.start() + _reset_tick_interval() + + return true + + +func is_lsp_connected() -> bool: + return _client.get_status() == StreamPeerTCP.STATUS_CONNECTED + + +## Sends a didOpen/didClose notification request, which results in publishDiagnostics reply being sent. +## Expects "res_path" to be proper res:// uri. +func update_diagnostics(res_path: String, content: String) -> void: + var uri := _res_path_to_lsp_uri(res_path) + + _send_notification("textDocument/didOpen", { + "textDocument": { + "uri": uri, + "text": content, + "languageId": "gdscript", # Unused by Godot LSP + "version": 0, # Unused by Godot LSP + } + }) + + # Technically, the Godot LS does nothing on didClose, but send it anyway in case it changes in the future. + _send_notification("textDocument/didClose", { + "textDocument": { + "uri": uri + } + }) + + +## Returns the absolute project path as reported by the LS. +func get_project_path() -> String: + return _lsp_project_path + + +func _reset_tick_interval() -> void: + _timer.start(TICK_INTERVAL_SECONDS_MIN) + + +func _update_tick_interval() -> void: + # Double the tick interval to gradiually reduce computation time when not in use. + _timer.wait_time = minf(_timer.wait_time * 2, TICK_INTERVAL_SECONDS_MAX) + + +func _on_tick() -> void: + if not _update_status(): + disconnect_lsp() + return + + _update_tick_interval() + + while _client.get_available_bytes(): + var json := _read_data() + + if json: + log_debug("Received message:\n%s" % json) + + _handle_response(json) + _reset_tick_interval() # Reset timer interval whenever data arrived as there will likely be more data coming + + +## Updates the current socket status and returns true when the main loop should continue. +func _update_status() -> bool: + var last_status := _client.get_status() + + _client.poll() + + var status := _client.get_status() + + match status: + StreamPeerTCP.STATUS_NONE: + return false + StreamPeerTCP.STATUS_ERROR: + log_error("StreamPeerTCP error") + return false + StreamPeerTCP.STATUS_CONNECTING: + pass + StreamPeerTCP.STATUS_CONNECTED: + # First time connected -> run initialization + if last_status != status: + log_debug("Connected to LSP") + on_connected.emit() + _initialize() + + return true + + +func _read_data() -> Dictionary: + # NOTE: + # At the moment, Godot only ever transmits headers with a single Content-Length field and + # likewise expects headers with only one field (see gdscript_language_protocol.cpp, line 61). + # Hence, the following also assumes there is only the Content-Length field in the header. + # If Godot ever starts sending additional fields, this will break. + + var header := _read_header().strip_edges() + var content_length := int(header.substr(len("Content-Length"))) + var content := _read_content(content_length) + var json: Dictionary = JSON.parse_string(content) + + if not json: + log_error("Failed to parse JSON: %s" % content) + return {} + + return json + + +func _read_content(length: int) -> String: + var data := _client.get_data(length) + + if data[0] != OK: + log_error("Failed to read content: %s" % error_string(data[0])) + return "" + else: + var buf: PackedByteArray = data[1] + return buf.get_string_from_utf8() + + +func _read_header() -> String: + var buf := PackedByteArray() + var char_r := "\r".unicode_at(0) + var char_n := "\n".unicode_at(0) + + while true: + var data := _client.get_data(1) + + if data[0] != OK: + log_error("Failed to read header: %s" % error_string(data[0])) + return "" + else: + buf.push_back(data[1][0]) + + var bufsize := buf.size() + + if bufsize >= 4 \ + and buf[bufsize - 1] == char_n \ + and buf[bufsize - 2] == char_r \ + and buf[bufsize - 3] == char_n \ + and buf[bufsize - 4] == char_r: + return buf.get_string_from_ascii() + + # This should never happen but the GDScript compiler complains "not all code paths return a value" + return "" + + +func _handle_response(json: Dictionary) -> void: + var method: String = json.get("method", "") + + match method: + # Diagnostics received + "textDocument/publishDiagnostics": + on_publish_diagnostics.emit(_parse_diagnostics(json["params"])) + return + + # Project path + "gdscript_client/changeWorkspace": + _lsp_project_path = str(json["params"]["path"]).simplify_path() + return + + # Initialization response + if json.get("id") == 0: + _send_notification("initialized", {}) + on_initialized.emit() + return + + # JSON-RPC error + if json.has("error"): + var error: Dictionary = json["error"] + log_error("JSON-RPC Error: %s" % error) + log_error("This is likely a bug in the plugin. Consider submitting a bug report on GitHub.") + on_jsonrpc_error.emit(error) + + +## Parses the diagnostic information according to the LSP specification. +## https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#publishDiagnosticsParams +func _parse_diagnostics(params: Dictionary) -> DiagnosticList_Diagnostic.Pack: + var result := DiagnosticList_Diagnostic.Pack.new() + result.res_uri = StringName(_lsp_uri_to_res_path(str(params["uri"]))) + + var diagnostics: Array[Dictionary] = [] + diagnostics.assign(params["diagnostics"]) + + for diag in diagnostics: + var range_start: Dictionary = diag["range"]["start"] + var entry := DiagnosticList_Diagnostic.new() + entry.res_uri = result.res_uri + entry.message = diag["message"] + entry.severity = (int(diag["severity"]) - 1) as DiagnosticList_Diagnostic.Severity # One-based in LSP, hence convert to the zero-based enum value + entry.line_start = int(range_start["line"]) + entry.column_start = int(range_start["character"]) + result.diagnostics.append(entry) + + return result + + +func _send_request(method: String, params: Dictionary) -> int: + _send(_jsonrpc.make_request(method, params, _id)) + _id += 1 + return _id - 1 + + +func _send_notification(method: String, params: Dictionary) -> void: + _send(_jsonrpc.make_notification(method, params)) + + +func _send(json: Dictionary) -> void: + var content := JSON.stringify(json, "", false) + var content_bytes := content.to_utf8_buffer() + var header := "Content-Length: %s\r\n\r\n" % len(content_bytes) + var header_bytes := header.to_ascii_buffer() + log_debug("Sending message (length: %s): %s" % [ len(content_bytes), content ]) + _client.put_data(header_bytes + content_bytes) + _reset_tick_interval() # Reset the timer interval because we are expecting a response + + +func _initialize() -> void: + _send_request("initialize", { + "processId": null, + "capabilities": { + "textDocument": { + "publishDiagnostics": {}, + }, + }, + }) + + +func _res_path_to_lsp_uri(res_path: String) -> String: + return URI_PREFIX + ProjectSettings.globalize_path(res_path).simplify_path() + + +func _lsp_uri_to_res_path(lsp_uri: String) -> String: + return ProjectSettings.localize_path(lsp_uri.replace(URI_PREFIX, "")) + + +func log_debug(text: String) -> void: + if ENABLE_DEBUG_LOG: + print("[DiagnosticList] ", text) + +func log_error(text: String) -> void: + push_error("[DiagnosticList] ", text) diff --git a/demo/addons/diagnosticlist/Panel.gd b/demo/addons/diagnosticlist/Panel.gd new file mode 100644 index 0000000..ca58824 --- /dev/null +++ b/demo/addons/diagnosticlist/Panel.gd @@ -0,0 +1,229 @@ +@tool +extends Control +class_name DiagnosticList_Panel + +class DiagnosticSeveritySettings extends RefCounted: + var text: String + var icon: Texture2D + var color: Color + + func _init(text_: String, icon_id: StringName, color_id: StringName) -> void: + self.text = text_ + self.icon = EditorInterface.get_editor_theme().get_icon(icon_id, &"EditorIcons") + self.color = EditorInterface.get_editor_theme().get_color(color_id, &"Editor") + + +@onready var _btn_refresh_errors: Button = %"btn_refresh_errors" +@onready var _error_list_tree: Tree = %"error_tree_list" +@onready var _cb_auto_refresh: CheckBox = %"cb_auto_refresh" +@onready var _cb_group_by_file: CheckBox = %"cb_group_by_file" +@onready var _label_refresh_time: Label = %"label_refresh_time" +@onready var _multiple_instances_alert: AcceptDialog = %"multiple_instances_alert" + +# This array will be filled according to each severity type to allow direct indexing +@onready var _filter_buttons: Array[Button] = [ + %"btn_filter_errors", + %"btn_filter_warnings", + %"btn_filter_infos", + %"btn_filter_hints", +] + +# This array will be filled according to each severity type to allow direct indexing +@onready var _severity_settings: Array[DiagnosticSeveritySettings] = [ + DiagnosticSeveritySettings.new("Error", &"StatusError", &"error_color"), + DiagnosticSeveritySettings.new("Warning", &"StatusWarning", &"warning_color"), + DiagnosticSeveritySettings.new("Info", &"Popup", &"font_color"), + DiagnosticSeveritySettings.new("Hint", &"Info", &"font_color"), +] + +@onready var _script_icon: Texture2D = get_theme_icon(&"Script", &"EditorIcons") + +var _provider: DiagnosticList_DiagnosticProvider + + +## Alternative to _ready(). This will be called by plugin.gd to ensure the code in here only runs +## when this script is loaded as part of the plugin and not while editing the scene. +func _plugin_ready() -> void: + for i in len(_filter_buttons): + var btn: Button = _filter_buttons[i] + var severity := _severity_settings[i] + btn.icon = severity.icon + + # These kinds of severities do not exist yet in Godot LSP, so hide them for now. + _filter_buttons[DiagnosticList_Diagnostic.Severity.Info].hide() + _filter_buttons[DiagnosticList_Diagnostic.Severity.Hint].hide() + + _cb_auto_refresh.button_pressed = DiagnosticList_Settings.get_auto_refresh() + + _error_list_tree.columns = 3 + _error_list_tree.set_column_title(0, "Message") + _error_list_tree.set_column_title(1, "File") + _error_list_tree.set_column_title(2, "Line") + _error_list_tree.set_column_title_alignment(0, HORIZONTAL_ALIGNMENT_LEFT) + _error_list_tree.set_column_title_alignment(1, HORIZONTAL_ALIGNMENT_LEFT) + _error_list_tree.set_column_title_alignment(2, HORIZONTAL_ALIGNMENT_LEFT) + + var line_column_size := _error_list_tree.get_theme_font("font").get_string_size( + "Line 0000", HORIZONTAL_ALIGNMENT_LEFT, -1, _error_list_tree.get_theme_font_size("font_size")) + + _error_list_tree.set_column_custom_minimum_width(0, 0) + _error_list_tree.set_column_custom_minimum_width(1, 0) + _error_list_tree.set_column_custom_minimum_width(2, int(line_column_size.x)) + + _error_list_tree.set_column_expand(0, true) + _error_list_tree.set_column_expand(1, true) + _error_list_tree.set_column_expand(2, false) + _error_list_tree.set_column_clip_content(0, true) + _error_list_tree.set_column_clip_content(1, true) + _error_list_tree.set_column_clip_content(2, false) + _error_list_tree.set_column_expand_ratio(0, 4) + + _multiple_instances_alert.add_button("More Information", true, "https://github.com/mphe/godot-diagnostic-list#does-not-work-correctly-with-multiple-godot-instances") + _multiple_instances_alert.custom_action.connect(func(action: StringName) -> void: OS.shell_open(action)) + _multiple_instances_alert.visible = false + + +## Called by plugin.gd when the LSPClient is ready +func start(provider: DiagnosticList_DiagnosticProvider) -> void: + _provider = provider + + # Now that it is safe to do stuff, connect all the signals + _provider.on_diagnostics_finished.connect(_on_diagnostics_finished) + _provider.on_update_progress.connect(_on_update_progress) + + _btn_refresh_errors.pressed.connect(_on_force_refresh) + _cb_group_by_file.toggled.connect(_on_group_by_file_toggled) + _cb_auto_refresh.toggled.connect(_on_auto_refresh_toggled) + _error_list_tree.item_activated.connect(_on_item_activated) + + for btn in _filter_buttons: + btn.toggled.connect(_on_filter_toggled) + + # Start checking + _set_status_string("", false) + _start_stop_auto_refresh() + + # If connected to a LS of a different Godot instance, show a warning + if provider.get_lsp_client().get_project_path() != ProjectSettings.globalize_path("res://").simplify_path(): + _multiple_instances_alert.popup_centered() + + +func refresh() -> void: + # NOTE: This list is sorted by file name as LSP publishes diagnostics per file + # This is important as the group-by-file implementation relies on it. + var diagnostics := _provider.get_diagnostics() + var group_by_file := _cb_group_by_file.button_pressed + + if not group_by_file: + diagnostics.sort_custom(_sort_by_severity) + + # Show refresh time + _set_status_string("Up-to-date", true) + + # Clear tree + _error_list_tree.clear() + _error_list_tree.create_item() + + # Create diagnostics + var last_uri: StringName + var parent: TreeItem = null + + for diag in diagnostics: + if not _filter_buttons[diag.severity].button_pressed: + continue + + # If grouping by file, create header entries if necessary + if group_by_file and diag.res_uri != last_uri: + last_uri = diag.res_uri + parent = _error_list_tree.create_item() + parent.set_text(0, diag.res_uri) + parent.set_icon(0, _script_icon) + parent.set_metadata(0, diag) + + _create_entry(diag, parent) + + # Update diagnostic counts + for i in len(_filter_buttons): + _filter_buttons[i].text = str(_provider.get_diagnostic_count(i)) + + +func _set_status_string(text: String, with_last_time: bool) -> void: + if with_last_time: + _label_refresh_time.text = "%s\n%.2f s" % [ text, _provider.get_refresh_time_usec() / 1000000.0 ] + else: + _label_refresh_time.text = text + + +func _sort_by_severity(a: DiagnosticList_Diagnostic, b: DiagnosticList_Diagnostic) -> bool: + if a.severity == b.severity: + return a.res_uri < b.res_uri + return a.severity < b.severity + + +func _create_entry(diag: DiagnosticList_Diagnostic, parent: TreeItem) -> void: + var entry: TreeItem = _error_list_tree.create_item(parent) + var severity_setting := _severity_settings[diag.severity] + # entry.set_custom_color(0, theme.color) + entry.set_text(0, diag.message) + entry.set_icon(0, severity_setting.icon) + entry.set_text(1, diag.get_filename()) + entry.set_tooltip_text(1, diag.res_uri) + # entry.set_text(2, "Line " + str(diag.line_start)) + entry.set_text(2, str(diag.line_start + 1)) + entry.set_metadata(0, diag) # Meta data is used in _on_item_activated to open the respective script + + +func _update_diagnostics(force: bool) -> void: + if _provider.is_updating() or _provider.refresh_diagnostics(force): + _set_status_string("Updating...", false) + else: + _set_status_string("Up-to-date", true) + + +func _start_stop_auto_refresh() -> void: + if _cb_auto_refresh.button_pressed: + visibility_changed.connect(_on_auto_update) + _provider.on_diagnostics_available.connect(_on_auto_update) + _on_auto_update() # Also trigger an update immediately + else: + visibility_changed.disconnect(_on_auto_update) + _provider.on_diagnostics_available.disconnect(_on_auto_update) + + +func _on_item_activated() -> void: + var selected: TreeItem = _error_list_tree.get_selected() + var diagnostic: DiagnosticList_Diagnostic = selected.get_metadata(0) + + # NOTE: Lines and columns are zero-based in LSP, but Godot expects one-based values + EditorInterface.edit_script(load(str(diagnostic.res_uri)), diagnostic.line_start + 1, diagnostic.column_start + 1) + + if not EditorInterface.get_editor_settings().get("text_editor/external/use_external_editor"): + EditorInterface.set_main_screen_editor("Script") + + +func _on_force_refresh() -> void: + _update_diagnostics(true) + + +func _on_auto_refresh_toggled(toggled_on: bool) -> void: + DiagnosticList_Settings.set_auto_refresh(toggled_on) + _start_stop_auto_refresh() + + +func _on_auto_update() -> void: + if is_visible_in_tree(): + _update_diagnostics(false) + + +func _on_update_progress(num_remaining: int, num_all: int) -> void: + _set_status_string("Updating...\n(%d/%d)" % [ num_all - num_remaining, num_all ], false) + + +func _on_diagnostics_finished() -> void: + refresh() + +func _on_filter_toggled(_toggled_on: bool) -> void: + refresh() + +func _on_group_by_file_toggled(_toggled_on: bool) -> void: + refresh() diff --git a/demo/addons/diagnosticlist/Panel.tscn b/demo/addons/diagnosticlist/Panel.tscn new file mode 100644 index 0000000..6ceb05f --- /dev/null +++ b/demo/addons/diagnosticlist/Panel.tscn @@ -0,0 +1,112 @@ +[gd_scene load_steps=2 format=3 uid="uid://tsfsnxbfcax6"] + +[ext_resource type="Script" path="res://addons/diagnosticlist/Panel.gd" id="1_fewy8"] + +[node name="DiagnosticsPanel" type="Control"] +custom_minimum_size = Vector2(250, 225) +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_fewy8") + +[node name="HBoxContainer" type="HBoxContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="error_tree_list" type="Tree" parent="HBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +columns = 3 +column_titles_visible = true +hide_root = true +select_mode = 1 +scroll_horizontal_enabled = false + +[node name="VBoxContainer" type="VBoxContainer" parent="HBoxContainer"] +layout_mode = 2 + +[node name="cb_auto_refresh" type="CheckBox" parent="HBoxContainer/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +button_pressed = true +text = "Auto-Refresh" + +[node name="btn_refresh_errors" type="Button" parent="HBoxContainer/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +auto_translate = false +text = "Refresh" + +[node name="HSeparator" type="HSeparator" parent="HBoxContainer/VBoxContainer"] +layout_mode = 2 + +[node name="cb_group_by_file" type="CheckBox" parent="HBoxContainer/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Group by file" + +[node name="btn_filter_errors" type="Button" parent="HBoxContainer/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +focus_mode = 0 +theme_type_variation = &"EditorLogFilterButton" +toggle_mode = true +button_pressed = true +text = "0" + +[node name="btn_filter_warnings" type="Button" parent="HBoxContainer/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +focus_mode = 0 +theme_type_variation = &"EditorLogFilterButton" +toggle_mode = true +button_pressed = true +text = "0" + +[node name="btn_filter_infos" type="Button" parent="HBoxContainer/VBoxContainer"] +unique_name_in_owner = true +visible = false +layout_mode = 2 +focus_mode = 0 +theme_type_variation = &"EditorLogFilterButton" +toggle_mode = true +button_pressed = true +text = "0" + +[node name="btn_filter_hints" type="Button" parent="HBoxContainer/VBoxContainer"] +unique_name_in_owner = true +visible = false +layout_mode = 2 +focus_mode = 0 +theme_type_variation = &"EditorLogFilterButton" +toggle_mode = true +button_pressed = true +text = "0" + +[node name="label_refresh_time" type="Label" parent="HBoxContainer/VBoxContainer"] +unique_name_in_owner = true +self_modulate = Color(1, 1, 1, 0.541176) +layout_mode = 2 +size_flags_vertical = 10 +text = "Connecting..." +horizontal_alignment = 2 +autowrap_mode = 2 + +[node name="multiple_instances_alert" type="AcceptDialog" parent="."] +unique_name_in_owner = true +title = "Diagnostic List - Warning!" +position = Vector2i(0, 36) +size = Vector2i(640, 158) +dialog_text = "There seems to be another Godot instance running. + +The Diagnostic List plugin does not work correctly with multiple Godot instances. +It will only be able to connect to the first instance, causing incorrect diagnostics." diff --git a/demo/addons/diagnosticlist/Settings.gd b/demo/addons/diagnosticlist/Settings.gd new file mode 100644 index 0000000..2e84a9f --- /dev/null +++ b/demo/addons/diagnosticlist/Settings.gd @@ -0,0 +1,20 @@ +extends RefCounted +class_name DiagnosticList_Settings + + +const BASE_SETTING_PATH = "addons/diagnostic_list/" +const SETTING_AUTO_REFRESH = BASE_SETTING_PATH + "auto_refresh" + + +static func set_auto_refresh(on: bool) -> void: + _set_setting(SETTING_AUTO_REFRESH, on) + + +static func get_auto_refresh() -> bool: + return ProjectSettings.get_setting(SETTING_AUTO_REFRESH, true) as bool + + +static func _set_setting(name: String, value: Variant) -> void: + ProjectSettings.set_setting(name, value) + ProjectSettings.set_as_internal(name, true) + ProjectSettings.save() diff --git a/demo/addons/diagnosticlist/plugin.cfg b/demo/addons/diagnosticlist/plugin.cfg new file mode 100644 index 0000000..3ebd98f --- /dev/null +++ b/demo/addons/diagnosticlist/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="Diagnostic List" +description="Provides a project-wide list of GDScript diagnostics." +author="Marvin Ewald" +version="1.0.3" +script="plugin.gd" diff --git a/demo/addons/diagnosticlist/plugin.gd b/demo/addons/diagnosticlist/plugin.gd new file mode 100644 index 0000000..1c9f8c3 --- /dev/null +++ b/demo/addons/diagnosticlist/plugin.gd @@ -0,0 +1,29 @@ +@tool +extends EditorPlugin + +const panel_scene = preload("res://addons/diagnosticlist/Panel.tscn") + +var _dock: DiagnosticList_Panel +var _client: DiagnosticList_LSPClient +var _provider: DiagnosticList_DiagnosticProvider + + +func _enter_tree() -> void: + _client = DiagnosticList_LSPClient.new(self) + _client.on_initialized.connect(_on_lsp_initialized) + _client.connect_lsp() + + _dock = panel_scene.instantiate() + _dock.ready.connect(func() -> void: _dock._plugin_ready()) + add_control_to_bottom_panel(_dock, "Diagnostics") + + +func _exit_tree() -> void: + remove_control_from_bottom_panel(_dock) + _dock.free() + _client.disconnect_lsp() + + +func _on_lsp_initialized() -> void: + _provider = DiagnosticList_DiagnosticProvider.new(_client) + _dock.start(_provider) diff --git a/demo/addons/ropesim/Rope.gd b/demo/addons/ropesim/Rope.gd index 4da0fdb..ed3bff9 100644 --- a/demo/addons/ropesim/Rope.gd +++ b/demo/addons/ropesim/Rope.gd @@ -4,9 +4,16 @@ class_name Rope # TODO: Split line rendering into a separate node +## Triggered when the rope has been registered at the NativeRopeServer. signal on_registered() + +## Triggered when the rope has been unregistered from the NativeRopeServer. signal on_unregistered() +## Triggered when the point count changes, i.e. when the number of segments changes. +signal on_point_count_changed() + + ## Pause the simulation. @export var pause: bool = false: set = _set_pause @@ -16,6 +23,13 @@ signal on_unregistered() ## Overall rope length. Will be distributed uniformly among all segments. @export var rope_length: float = 100: set = _set_length +## Maximum euclidean distance between rope endpoints. Zero or negative for no limitation. +## This is an approximation and not 100% accurate. +## It is intended as a simple way to constraint the rope length when both endpoints are fixed by a RopeHandle. +## The actual length of the rope might differ depending on the number of constraint iterations. +## Fixed points in between are not taken into account. +@export var max_endpoint_distance: float = -1 + ## (Optional) Allows to distribute the length of rope segment in a non-uniform manner. ## Useful when certain parts of the rope should be more detailed than the rest. ## For example, if it is known that most movement happens at the beginning of the rope, a curve with @@ -24,8 +38,11 @@ signal on_unregistered() @export var segment_length_distribution: Curve: set = _set_seg_dist ## Stiffness forces the rope to return to its resting position. +## The resting direction is downwards and affected by the the node's rotation. +## Might not produce 100% realistic results with fixed points. @export var stiffness: float = 0.0 +## Gravity @export var gravity: float = 100 ## Gravity direction. Will not be normalized. @@ -38,19 +55,33 @@ signal on_unregistered() @export var damping_curve: Curve ## Constraints the rope to its intended length. Less constraint iterations effectively makes the rope more elastic. -@export var num_constraint_iterations: int = 10 +@export_range(0, 1000) var num_constraint_iterations: int = 10 + +## Whether to fixate the first point at the rope's node position. +@export var fixate_begin: bool = true +## Render rope points for debugging purposes. @export var render_debug: bool = false: set = _set_draw_debug + +## Render the rope as line. @export var render_line: bool = true: set = _set_render_line + +## Rendered line width. @export var line_width: float = 2: set = _set_line_width + +## Rendered line color. @export var color: Color = Color.WHITE: set = _set_color + +## Color gradient along the rendered line. @export var color_gradient: Gradient: set = _set_gradient +var _registered: bool = false var _colors := PackedColorArray() var _seg_lengths := PackedFloat32Array() var _points := PackedVector2Array() var _oldpoints := PackedVector2Array() -var _registered: bool = false +# NOTE: Not @exported on purpose to prevent accidentally saving a scene with unintended weights, e.g. due to bugs or user errors. +var _simulation_weights := PackedFloat32Array() # General @@ -75,7 +106,6 @@ func _draw() -> void: draw_set_transform_matrix(get_global_transform().affine_inverse()) if render_line and _points.size() > 1: - _points[0] = global_position if color_gradient: draw_polyline_colors(_points, _colors, line_width) else: @@ -94,6 +124,8 @@ func _setup(run_reset: bool = true) -> void: _points.resize(num_segments + 1) _oldpoints.resize(num_segments + 1) + _resize_with_default(_simulation_weights, num_segments + 1, 1.0) + update_colors() update_segments() @@ -132,27 +164,27 @@ func _start_stop_rendering() -> void: queue_redraw() -func _register_server(): +func _register_server() -> void: if not _registered: NativeRopeServer.register_rope(self) - emit_signal("on_registered") + on_registered.emit() if render_debug or render_line: - NativeRopeServer.connect("on_post_update", Callable(self, "_on_post_update")) # warning-ignore: return_value_discarded + NativeRopeServer.on_post_update.connect(_on_post_update) _registered = true -func _unregister_server(): +func _unregister_server() -> void: if _registered: NativeRopeServer.unregister_rope(self) - emit_signal("on_unregistered") - if NativeRopeServer.is_connected("on_post_update", Callable(self, "_on_post_update")): - NativeRopeServer.disconnect("on_post_update", Callable(self, "_on_post_update")) + on_unregistered.emit() + if NativeRopeServer.on_post_update.is_connected(_on_post_update): + NativeRopeServer.on_post_update.disconnect(_on_post_update) _registered = false # Cache line colors according to color and color_gradient. # Usually, you should not need to call this manually. -func update_colors(): +func update_colors() -> void: if not color_gradient: return @@ -167,23 +199,23 @@ func update_colors(): # Recompute segment lengths according to rope_length, num_segments and segment_length_distribution curve. # Usually, you should not need to call this manually. -func update_segments(): +func update_segments() -> void: if _seg_lengths.size() != num_segments: _seg_lengths.resize(num_segments) - if segment_length_distribution: - var length = 0.0 + if segment_length_distribution and segment_length_distribution.point_count > 0: + var length := 0.0 for i in _seg_lengths.size(): _seg_lengths[i] = segment_length_distribution.sample(get_point_perc(i + 1)) length += _seg_lengths[i] - var scaling = rope_length / length + var scaling := rope_length / length for i in _seg_lengths.size(): _seg_lengths[i] *= scaling else: - var base_seg_length = rope_length / num_segments + var base_seg_length := rope_length / num_segments for i in _seg_lengths.size(): _seg_lengths[i] = base_seg_length @@ -195,14 +227,22 @@ func get_num_points() -> int: return _points.size() +## Returns the point index at the given percentage (0.0 - 1.0) in related to the total amount of points of the rope. +## Does not incorporate segment lengths. If there is a rope with 10 points and the last segment +## spans 50% of the whole rope, then get_point_index(0.5) returns 4, not 9. func get_point_index(position_percent: float) -> int: - return int((get_num_points() - 1) * clamp(position_percent, 0, 1)) + return int((get_num_points() - 1) * clampf(position_percent, 0, 1)) +## Inverse of get_point_index(). Returns at which percentage the index is located. +## Does not incorporate segment lengths. func get_point_perc(index: int) -> float: return index / float(_points.size() - 1) if _points.size() > 0 else 0.0 +## Similar to get_point_index() but returns the coordinates of the point at the given position fraction. +## If the target position lies between two points, the result will be interpolated. +## Does not incorporate segment lengths. func get_point_interpolate(position_perc: float) -> Vector2: var idx := get_point_index(position_perc) if idx == _points.size() - 1: @@ -213,18 +253,20 @@ func get_point_interpolate(position_perc: float) -> Vector2: return lerp(_points[idx], _points[next], (position_perc - perc) / (next_perc - perc)) +## Returns the point index nearest to the given coordinates. func get_nearest_point_index(pos: Vector2) -> int: - var min_dist = 1e10 - var idx = 0 + var min_dist := 1e10 + var idx := 0 for i in _points.size(): - var dist = pos.distance_squared_to(_points[i]) + var dist := pos.distance_squared_to(_points[i]) if dist < min_dist: min_dist = dist idx = i return idx + func get_point(index: int) -> Vector2: return _points[index] @@ -237,12 +279,10 @@ func move_point(index: int, vec: Vector2) -> void: _points[index] += vec -# Makes a copy! PoolVector2Array is pass-by-value. func get_points() -> PackedVector2Array: return _points -# Makes a copy! PoolVector2Array is pass-by-value. func get_old_points() -> PackedVector2Array: return _oldpoints @@ -252,7 +292,7 @@ func get_segment_length(segment_index: int) -> float: func reset(dir: Vector2 = Vector2.DOWN) -> void: - # TODO: Reset in global_transform direction + # TODO: Reset in global_transform or gravity_direction direction _points[0] = (global_position if is_inside_tree() else position) for i in range(1, _points.size()): _points[i] = _points[i - 1] + dir * get_segment_length(i - 1) @@ -278,49 +318,71 @@ func get_segment_lengths() -> PackedFloat32Array: return _seg_lengths +## The simulation weight determines how much a point can be moved during the simulation/constraint phase. +## 0.0 means no movement at all, i.e. the point is fixed. +## 1.0 allows full movement. +func set_point_simulation_weight(index: int, weight: float) -> void: + _simulation_weights[index] = clampf(weight, 0.0, 1.0) + + +func get_point_simulation_weight(index: int) -> float: + return _simulation_weights[index] + + # Setters -func _set_num_segs(value: int): +func _set_num_segs(value: int) -> void: + if value == num_segments: + return num_segments = value - _setup(false) + _setup(Engine.is_editor_hint()) + on_point_count_changed.emit() -func _set_length(value: float): +func _set_length(value: float) -> void: rope_length = value - _setup(false) + _setup(Engine.is_editor_hint()) -func _set_draw_debug(value: bool): +func _set_draw_debug(value: bool) -> void: render_debug = value _start_stop_rendering() -func _set_render_line(value: bool): +func _set_render_line(value: bool) -> void: render_line = value _start_stop_rendering() -func _set_line_width(value: float): +func _set_line_width(value: float) -> void: line_width = value queue_redraw() -func _set_color(value: Color): +func _set_color(value: Color) -> void: color = value update_colors() + queue_redraw() -func _set_pause(value: bool): +func _set_pause(value: bool) -> void: pause = value _start_stop_process() -func _set_gradient(value: Gradient): +func _set_gradient(value: Gradient) -> void: if color_gradient: - color_gradient.disconnect("changed", Callable(self, "update_colors")) + color_gradient.changed.disconnect(update_colors) color_gradient = value if color_gradient: - color_gradient.connect("changed", Callable(self, "update_colors")) # warning-ignore: return_value_discarded + color_gradient.changed.connect(update_colors) update_colors() -func _set_seg_dist(value: Curve): +func _set_seg_dist(value: Curve) -> void: if segment_length_distribution: - segment_length_distribution.disconnect("changed", Callable(self, "update_segments")) + segment_length_distribution.changed.disconnect(update_segments) segment_length_distribution = value if segment_length_distribution: - segment_length_distribution.connect("changed", Callable(self, "update_segments")) # warning-ignore: return_value_discarded + segment_length_distribution.changed.connect(update_segments) update_segments() + +func _resize_with_default(arr: PackedFloat32Array, new_size: int, default: float) -> void: + var oldsize := arr.size() + arr.resize(new_size) + + for i in range(oldsize, new_size): + arr[i] = default diff --git a/demo/addons/ropesim/RopeAnchor.gd b/demo/addons/ropesim/RopeAnchor.gd index c812411..e66c690 100644 --- a/demo/addons/ropesim/RopeAnchor.gd +++ b/demo/addons/ropesim/RopeAnchor.gd @@ -2,17 +2,25 @@ extends Marker2D class_name RopeAnchor -# Gets emitted just after applying the position. +## Can be used to attach nodes at certain positions on a target rope. + +## Gets emitted just after applying the position. This happens always during _physics_process(). signal on_after_update() @export var force_update: bool: set = _set_force_update -@export var enable: bool = true: get = get_enable, set = set_enable # Enable or disable. -@export var rope_path: NodePath: set = set_rope_path -@export var rope_position = 1.0 # Position on the rope between 0 and 1. # (float, 0, 1) -@export var apply_angle := false # Also apply rotation according to the rope curvature. +## Enable or disable. +@export var enable: bool = true: get = get_enable, set = set_enable +## Target rope node. +@export_node_path("Rope") var rope_path: NodePath: set = set_rope_path +## Position on the rope between 0 and 1. +@export_range(0.0, 1.0) var rope_position: float = 1.0 +## Also apply rotation according to the rope curvature. +@export var apply_angle := false ## If false, only consider the nearest vertex on the rope. Otherwise, interpolate the position between two relevant points when applicable. @export var precise: bool = false + var _helper: RopeToolHelper +var _last_pos: Vector2 func _init() -> void: @@ -28,16 +36,17 @@ func _ready() -> void: func _on_post_update() -> void: _update() - emit_signal("on_after_update") + on_after_update.emit() -func set_rope_path(value: NodePath): +func set_rope_path(value: NodePath) -> void: rope_path = value + if is_inside_tree(): - _helper.target_rope = get_node(rope_path) as Rope + _helper.set_target_rope_path(rope_path, self) -func set_enable(value: bool): +func set_enable(value: bool) -> void: enable = value _helper.enable = value @@ -46,9 +55,16 @@ func get_enable() -> bool: return _helper.enable +## Returns the difference between the last and current position. +func get_velocity() -> Vector2: + return global_position - _last_pos + + func _update() -> void: var rope: Rope = _helper.target_rope + _last_pos = global_position + if precise: global_position = rope.get_point_interpolate(rope_position) else: @@ -61,5 +77,5 @@ func _update() -> void: func _set_force_update(_val: bool) -> void: - if Engine.is_editor_hint() and _helper.target_rope: + if _helper.target_rope: _update() diff --git a/demo/addons/ropesim/RopeCollisionShapeGenerator.gd b/demo/addons/ropesim/RopeCollisionShapeGenerator.gd index f025ae5..65d667b 100644 --- a/demo/addons/ropesim/RopeCollisionShapeGenerator.gd +++ b/demo/addons/ropesim/RopeCollisionShapeGenerator.gd @@ -2,15 +2,17 @@ extends Node class_name RopeCollisionShapeGenerator -# Populates the parent with CollisionShape2Ds with a SegmentShape2D to fit the target rope. -# It can be added as child to an Area2D for example, to detect if something collides with the rope. -# It does _not_ make the rope interact with other physics objects. +## Populates the parent with CollisionShape2Ds with a SegmentShape2D to fit the target rope. +## It can be added as child to an Area2D for example, to detect if something collides with the rope. +## It does _not_ make the rope interact with other physics objects. -@export var enable: bool = true: get = get_enable, set = set_enable # Enable or disable. -@export var rope_path: NodePath: set = set_rope_path +## Enable or disable. +@export var enable: bool = true: get = get_enable, set = set_enable +## Target rope node. +@export_node_path("Rope") var rope_path: NodePath: set = set_rope_path var _helper: RopeToolHelper -var _colliders := [] # Array[CollisionShape2D] +var _colliders: Array[CollisionShape2D] = [] func _init() -> void: @@ -32,14 +34,14 @@ func _on_post_update() -> void: _update_shapes() -func set_rope_path(value: NodePath): +func set_rope_path(value: NodePath) -> void: rope_path = value if is_inside_tree(): _helper.target_rope = get_node(rope_path) as Rope _build() -func set_enable(value: bool): +func set_enable(value: bool) -> void: enable = value _helper.enable = value @@ -63,7 +65,7 @@ func _build() -> void: func _enable_shapes(num: int) -> void: - var diff = num - _colliders.size() + var diff := num - _colliders.size() if diff > 0: for i in diff: @@ -72,12 +74,13 @@ func _enable_shapes(num: int) -> void: _colliders.append(shape) get_parent().call_deferred("add_child", shape) elif diff < 0: - for i in abs(diff): + for i in absi(diff): + @warning_ignore("unsafe_method_access") _colliders.pop_back().queue_free() func _update_shapes() -> void: - var points = _helper.target_rope.get_points() + var points := _helper.target_rope.get_points() for i in _colliders.size(): var shape: CollisionShape2D = _colliders[i] diff --git a/demo/addons/ropesim/RopeHandle.gd b/demo/addons/ropesim/RopeHandle.gd index 43ae13d..a99448d 100644 --- a/demo/addons/ropesim/RopeHandle.gd +++ b/demo/addons/ropesim/RopeHandle.gd @@ -2,22 +2,36 @@ extends Marker2D class_name RopeHandle -# Gets emitted just before applying the position. +## Can be used to control, animate or fixate points on a target rope. + +## Gets emitted just before applying the position. This happens always during _physics_process(). signal on_before_update() -@export var enable: bool = true: get = get_enable, set = set_enable # Enable or disable -@export var rope_path: NodePath: set = set_rope_path -@export var rope_position = 1.0 # Position on the rope between 0 and 1. # (float, 0, 1) -@export var smoothing: bool = false # Whether to smoothly snap to RopeHandle's position instead of instantly. -@export var position_smoothing_speed: float = 0.5 # Smoothing speed +## Enable or disable +@export var enable: bool = true: get = get_enable, set = set_enable +## Target rope node. +@export_node_path("Rope") var rope_path: NodePath: set = set_rope_path +## Position on the rope between 0 and 1. +@export_range(0.0, 1.0) var rope_position: float = 1.0 : set = set_rope_position +## Whether to smoothly snap to RopeHandle's position instead of instantly. +@export var smoothing: bool = false +## Smoothing speed +@export var position_smoothing_speed: float = 0.5 ## If false, only affect the nearest vertex on the rope. Otherwise, affect both surrounding points when applicable. @export var precise: bool = false +## Determines how much the target point is allowed to move. A value of 0.0 sets the point's position +## but it is still fully affected by simulation and constraining. +## A value of 1.0 completely fixates the point at the handle's position and allows no further movement. +@export_range(0.0, 1.0) var strength: float = 0.0 : set = set_strength + var _helper: RopeToolHelper +var _target_idx: int = 0 func _init() -> void: if not _helper: _helper = RopeToolHelper.new(RopeToolHelper.UPDATE_HOOK_PRE, self, "_on_pre_update") + _helper.on_rope_assigned.connect(_on_rope_assigned) add_child(_helper) @@ -26,25 +40,32 @@ func _ready() -> void: set_enable(enable) +func _enter_tree() -> void: + _update_state(null) + + +func _exit_tree() -> void: + _restore_state(_helper.target_rope) + + func _on_pre_update() -> void: - emit_signal("on_before_update") + on_before_update.emit() var rope: Rope = _helper.target_rope - var point_index: int = rope.get_point_index(rope_position) # Only use this method if this is not the last point. - if precise and point_index < rope.get_num_points() - 1: + if precise and _target_idx < rope.get_num_points() - 1: # TODO: Consider creating a corresponding function in Rope.gd for universal access, e.g. set_point_interpolated(). var point_pos: Vector2 = rope.get_point_interpolate(rope_position) var diff := global_position - point_pos - var pos_a: Vector2 = rope.get_point(point_index) - var pos_b: Vector2 = rope.get_point(point_index + 1) + var pos_a: Vector2 = rope.get_point(_target_idx) + var pos_b: Vector2 = rope.get_point(_target_idx + 1) var new_pos_a: Vector2 = pos_a + diff var new_pos_b: Vector2 = pos_b + diff - _move_point(point_index, pos_a, new_pos_a) - _move_point(point_index + 1, pos_b, new_pos_b) + _move_point(_target_idx, pos_a, new_pos_a) + _move_point(_target_idx + 1, pos_b, new_pos_b) else: - _move_point(point_index, rope.get_point(point_index), global_position) + _move_point(_target_idx, rope.get_point(_target_idx), global_position) func _move_point(idx: int, from: Vector2, to: Vector2) -> void: @@ -53,15 +74,64 @@ func _move_point(idx: int, from: Vector2, to: Vector2) -> void: _helper.target_rope.set_point(idx, to) -func set_rope_path(value: NodePath): +func set_rope_path(value: NodePath) -> void: rope_path = value + if is_inside_tree(): - _helper.target_rope = get_node(rope_path) as Rope + _helper.set_target_rope_path(rope_path, self) -func set_enable(value: bool): +func set_enable(value: bool) -> void: + if enable == value: + return + enable = value _helper.enable = value + if not enable: + _restore_state(_helper.target_rope) + else: + _update_state(null) + + func get_enable() -> bool: return _helper.enable + + +func set_strength(value: float) -> void: + strength = value + _update_state_current_rope() + + +func set_rope_position(value: float) -> void: + rope_position = value + _update_state_current_rope() + + +func _on_rope_assigned(old: Rope) -> void: + _update_state(old) + + +func _update_state_current_rope() -> void: + _update_state(_helper.target_rope) + + +func _update_state(old_rope: Rope) -> void: + if not enable: + return + + _restore_state(old_rope) + + var rope := _helper.target_rope + + # Compute and apply new state + if rope: + _target_idx = rope.get_point_index(rope_position) + # TODO: Maybe set this value in _on_pre_update() so if it gets overwritten by another + # handle, it will be automatically restored when the other handle targets a different position again. + rope.set_point_simulation_weight(_target_idx, 1.0 - clampf(strength, 0.0, 1.0)) + + +func _restore_state(rope: Rope) -> void: + if rope: + rope.set_point_simulation_weight(_target_idx, 1.0) diff --git a/demo/addons/ropesim/RopeInteraction.gd b/demo/addons/ropesim/RopeInteraction.gd new file mode 100644 index 0000000..c8609ca --- /dev/null +++ b/demo/addons/ropesim/RopeInteraction.gd @@ -0,0 +1,166 @@ +@tool +extends Node +class_name RopeInteraction + +## Handles mutual interaction of a target node with a rope. +## Useful for rope grabbing or pulling mechanics where an object should be able to affect the rope +## while also being constrained by it. +## Uses [RopeHandle] and [RopeAnchor] internally. + +## Emitted when the target node should be moved and [member RopeInteraction.position_update_mode] is [enum RopeInteraction.Signal]. +signal on_movement_request(target: Node2D, anchor: RopeAnchor) + + +## Determines how the position of the target node is updated. +enum PositionUpdateMode { + ## Set [member Node2D.global_position] directly. + SetGlobalPosition, + + ## Use [method CharacterBody2D.move_and_slide]. Only applicable to [CharacterBody2D] targets. + MoveAndSlide, + + ## Do not set the position automatically, but emit the [signal RopeInteraction.on_movement_request] + ## signal to allow manual handling. + EmitSignal, +} + +## Enable or disable. +@export var enable: bool = true : set = set_enable + +## Determines how the position of the target node is updated. +@export var position_update_mode: PositionUpdateMode = PositionUpdateMode.EmitSignal + +## Target node that should be attached to the rope. +@export var target_node: Node2D + +## Target rope. +# @export_node_path("Rope") var rope: NodePath : set = set_rope +@export var rope: Rope : set = set_rope + +## Position on the rope between 0 and 1. +@export_range(0.0, 1.0) var rope_position: float = 1.0 : set = set_rope_position + +## Handle strength. See also [member RopeHandle.strength]. +## Usually only useful when the rope_position is either 0.0 or 1.0, i.e. one of the endpoints. +@export_range(0.0, 1.0) var strength: float = 0.0 : set = set_strength + +var _anchor: RopeAnchor +var _handle: RopeHandle + + +func _init() -> void: + _handle = _create_default_handle() + _handle.on_before_update.connect(_on_before_update) + add_child(_handle) + + _anchor = _create_default_anchor() + _anchor.on_after_update.connect(_on_after_update) + add_child(_anchor) + + +func _enter_tree() -> void: + set_rope(rope) + + +func _on_before_update() -> void: + _handle.global_position = target_node.global_position + + +func _on_after_update() -> void: + if position_update_mode == PositionUpdateMode.EmitSignal: + on_movement_request.emit(target_node, _anchor) + return + + var diff := _anchor.global_position - target_node.global_position + + if diff.length_squared() < 0.01 * 0.01: + return + + if position_update_mode == PositionUpdateMode.SetGlobalPosition: + target_node.global_position = _anchor.global_position + else: + var body := target_node as CharacterBody2D + + if not body: + push_error("RopeInteraction: Target node is not a CharacterBody2D") + return + + var backup_vel := body.velocity + # Counteract the delta multiplication that happens in move_and_slide() because we want to travel the whole distance + body.velocity = diff / get_physics_process_delta_time() + body.move_and_slide() + body.velocity = backup_vel + + +## Determine the nearest position on the rope to the target node and use it as [member RopeInteraction.rope_position]. +func use_nearest_position() -> void: + # TODO: Determine precise percentage, not just nearest index + var idx := rope.get_nearest_point_index(target_node.global_position) + var perc := rope.get_point_perc(idx) + rope_position = perc + + +## Snaps the target node to the current position on the rope. +func force_snap_to_rope() -> void: + _anchor.force_update = true # TODO: This should be a function to call + _on_after_update() + + +func set_rope_position(value: float) -> void: + rope_position = value + _handle.rope_position = value + _anchor.rope_position = value + + +# func set_rope(value: NodePath) -> void: +func set_rope(value: Rope) -> void: + rope = value + + if rope: + # NOTE: For some reason, rope.get_path() will result in unrecoverable path errors when the + # RopeInteraction node is moved in the tree. Using a relative path works fine. + # Maybe a Godot bug. + var path := _handle.get_path_to(rope) + _handle.rope_path = path + _anchor.rope_path = path + else: + _handle.rope_path = "" + _anchor.rope_path = "" + + +func set_enable(value: bool) -> void: + enable = value + _handle.enable = enable + _anchor.enable = enable + + +func set_strength(value: float) -> void: + strength = value + _handle.strength = strength + + +## Returns the internal [RopeAnchor]. +func get_anchor() -> RopeAnchor: + return _anchor + + +## Returns the internal [RopeHandle]. +func get_handle() -> RopeHandle: + return _handle + + +func _create_default_handle() -> RopeHandle: + var handle := RopeHandle.new() + handle.enable = true + handle.rope_position = rope_position + handle.precise = true + handle.strength = strength + return handle + + +func _create_default_anchor() -> RopeAnchor: + var anchor := RopeAnchor.new() + anchor.enable = true + anchor.rope_position = rope_position + anchor.precise = true + return anchor diff --git a/demo/addons/ropesim/RopeRendererLine2D.gd b/demo/addons/ropesim/RopeRendererLine2D.gd index d5fe5ae..aca8aa4 100644 --- a/demo/addons/ropesim/RopeRendererLine2D.gd +++ b/demo/addons/ropesim/RopeRendererLine2D.gd @@ -2,14 +2,40 @@ extends Line2D class_name RopeRendererLine2D +enum PositionMode { + ## Render the rope at this node's position, regardless of where the rope actually resides. + UseLineRendererPosition, + + ## Render the rope at the rope node's position. If the rope's beginning endpoint is not fixed, + ## the rope could actually reside somewhere else than its node. + UseRopeNodePosition, + + ## Render the rope at the position of the rope's first point. This is the most accurate + ## positioning method and will always correspond to the ropes true global position. + UseRopeFirstPointPosition, +} + const UPDATE_HOOK = "on_post_update" const HOOK_FUNC = "refresh" +## Convenience property that can be pressed in editor to force an update. @export var force_update: bool: set = _force_update -@export var target_rope_path: NodePath = "..": set = set_rope_path -@export var keep_rope_position: bool = true: set = _set_keep_pos + +## Target rope. +@export_node_path("Rope") var target_rope_path: NodePath = "..": set = set_rope_path + +## Determines at which position to render the rope. +@export var position_mode: PositionMode = PositionMode.UseRopeFirstPointPosition : set = set_position_mode + +## Update automatically. If disabled, a manual call to [method RopeRendererLine2D.refresh] is needed +## to update the line rendering. @export var auto_update: bool = true: get = get_auto_update, set = set_auto_update + +## Inverts the point order. The shape of the rope remains the same but the texture will repeat +## towards the beginning rather than the end. +## Useful when dynamically changing the rope's length. @export var invert: bool = false + var _helper: RopeToolHelper @@ -22,7 +48,6 @@ func _init() -> void: func _ready() -> void: set_rope_path(target_rope_path) set_auto_update(auto_update) - refresh() func refresh() -> void: @@ -31,13 +56,13 @@ func refresh() -> void: if target and target.get_num_points() > 0 and visible: var xform: Transform2D - if keep_rope_position: - if Engine.is_editor_hint(): + match position_mode: + PositionMode.UseLineRendererPosition: + xform = Transform2D(0, -target.get_point(0)) + PositionMode.UseRopeNodePosition: xform = Transform2D(0, -global_position - target.get_point(0) + target.global_position) - else: + PositionMode.UseRopeFirstPointPosition: xform = Transform2D(0, -global_position) - else: - xform = Transform2D(0, -target.get_point(0)) xform = xform.scaled(scale) var p: PackedVector2Array = xform * target.get_points() @@ -49,23 +74,21 @@ func refresh() -> void: global_rotation = 0 -func set_rope_path(value: NodePath): +func set_rope_path(value: NodePath) -> void: target_rope_path = value if is_inside_tree(): - _helper.target_rope = get_node(target_rope_path) as Rope - refresh() + _helper.set_target_rope_path(target_rope_path, self) -func _force_update(_value: bool): +func _force_update(_value: bool) -> void: refresh() -func _set_keep_pos(value: bool): - keep_rope_position = value - refresh() +func set_position_mode(value: PositionMode) -> void: + position_mode = value -func set_auto_update(value: bool): +func set_auto_update(value: bool) -> void: _helper.enable = value func get_auto_update() -> bool: diff --git a/demo/addons/ropesim/RopeToolHelper.gd b/demo/addons/ropesim/RopeToolHelper.gd index af34df6..b04ac6d 100644 --- a/demo/addons/ropesim/RopeToolHelper.gd +++ b/demo/addons/ropesim/RopeToolHelper.gd @@ -1,12 +1,15 @@ extends Node class_name RopeToolHelper -# This node should be used programmatically as helper in other rope tools. -# It contains boilerplate for registering/unregistering to/from NativeRopeServer when needed. +## This node should be used programmatically as helper in other rope tools. +## It contains boilerplate for registering/unregistering to/from NativeRopeServer when needed. const UPDATE_HOOK_POST = "on_post_update" const UPDATE_HOOK_PRE = "on_pre_update" +## Emitted when the assigned rope has been changed, i.e. to a new rope or null. +signal on_rope_assigned(old: Rope) + @export var enable: bool = true: set = set_enable var target_rope: Rope: set = set_target_rope @@ -31,11 +34,11 @@ func _exit_tree() -> void: func _unregister_server() -> void: if _is_registered(): - NativeRopeServer.disconnect(_update_hook, Callable(self, "_on_update")) + NativeRopeServer.disconnect(_update_hook, _on_update) func _is_registered() -> bool: - return NativeRopeServer.is_connected(_update_hook, Callable(self, "_on_update")) + return NativeRopeServer.is_connected(_update_hook, _on_update) func _on_update() -> void: @@ -48,7 +51,7 @@ func start_stop_process() -> void: # NOTE: It sounds smart to disable this helper if the rope is paused, but maybe there are exceptions. if enable and is_inside_tree() and target_rope and not target_rope.pause: if not _is_registered(): - NativeRopeServer.connect(_update_hook, Callable(self, "_on_update")) + NativeRopeServer.connect(_update_hook, _on_update) else: _unregister_server() @@ -63,13 +66,33 @@ func set_target_rope(value: Rope) -> void: return if target_rope and is_instance_valid(target_rope): - target_rope.disconnect("on_registered", Callable(self, "start_stop_process")) - target_rope.disconnect("on_unregistered", Callable(self, "start_stop_process")) + target_rope.on_registered.disconnect(start_stop_process) + target_rope.on_unregistered.disconnect(start_stop_process) + var old := target_rope target_rope = value if target_rope and is_instance_valid(target_rope): - target_rope.connect("on_registered", Callable(self, "start_stop_process")) # warning-ignore: return_value_discarded - target_rope.connect("on_unregistered", Callable(self, "start_stop_process")) # warning-ignore: return_value_discarded + target_rope.on_registered.connect(start_stop_process) + target_rope.on_unregistered.connect(start_stop_process) start_stop_process() + on_rope_assigned.emit(old) + + +## Set the target rope using a NodePath. Allows empty paths and treats them as null. +## If [param path_relative_node] is given, the path will be resolved relative to that node. +func set_target_rope_path(rope_path: NodePath, path_relative_node: Node = null) -> void: + if not is_inside_tree(): + push_warning("RopeToolHelper: Trying to assign rope but not added to tree") + return + + var node: Rope = null + + if rope_path: + if path_relative_node: + node = path_relative_node.get_node(rope_path) as Rope + else: + node = get_node(rope_path) as Rope + + set_target_rope(node) diff --git a/demo/addons/ropesim/plugin.gd b/demo/addons/ropesim/plugin.gd index 4f6f4f8..26cf85d 100644 --- a/demo/addons/ropesim/plugin.gd +++ b/demo/addons/ropesim/plugin.gd @@ -23,7 +23,8 @@ func _handles(object: Object) -> bool: object is RopeAnchor or object is RopeHandle or object is RopeCollisionShapeGenerator or - object is RopeRendererLine2D + object is RopeRendererLine2D or + object is RopeInteraction ) @@ -36,7 +37,7 @@ func _build_gui() -> void: _menu_toolbox.hide() add_control_to_container(EditorPlugin.CONTAINER_CANVAS_EDITOR_MENU, _menu_toolbox) - var menu_button = MenuButton.new() + var menu_button := MenuButton.new() menu_button.text = "Ropesim" _menu_toolbox.add_child(menu_button) _menu_popup = menu_button.get_popup() @@ -48,7 +49,7 @@ func _build_gui() -> void: func _menu_item_clicked(idx: int) -> void: match idx: MENU_INDEX_UPDATE_IN_EDITOR: - var value = not _menu_popup.is_item_checked(idx) + var value := not _menu_popup.is_item_checked(idx) _menu_popup.set_item_checked(MENU_INDEX_UPDATE_IN_EDITOR, value) NativeRopeServer.update_in_editor = value diff --git a/demo/project.godot b/demo/project.godot index f1ac743..ebb0772 100644 --- a/demo/project.godot +++ b/demo/project.godot @@ -11,17 +11,20 @@ config_version=5 [application] config/name="ropesim example" -run/main_scene="res://ropesim_demo.tscn" config/features=PackedStringArray("4.2") -config/icon="res://icon.png" +config/icon="res://rope_examples/icon.svg" [debug] gdscript/warnings/exclude_addons=false +gdscript/warnings/untyped_declaration=1 +gdscript/warnings/unsafe_property_access=1 +gdscript/warnings/unsafe_method_access=1 +gdscript/warnings/unsafe_call_argument=1 [editor_plugins] -enabled=PackedStringArray("res://addons/ropesim/plugin.cfg") +enabled=PackedStringArray("res://addons/diagnosticlist/plugin.cfg", "res://addons/ropesim/plugin.cfg") [gui] diff --git a/demo/rope_examples/anchors.tscn b/demo/rope_examples/anchors.tscn new file mode 100644 index 0000000..a169c7b --- /dev/null +++ b/demo/rope_examples/anchors.tscn @@ -0,0 +1,92 @@ +[gd_scene load_steps=8 format=3 uid="uid://l03q4o7r2nuk"] + +[ext_resource type="Script" path="res://rope_examples/scripts/animation_player.gd" id="1_sodim"] +[ext_resource type="Script" path="res://addons/ropesim/RopeAnchor.gd" id="2_hm10y"] +[ext_resource type="Texture2D" uid="uid://criwv6nuivcxy" path="res://rope_examples/icon.svg" id="3_y0u5w"] +[ext_resource type="Script" path="res://addons/ropesim/Rope.gd" id="4_jxxrc"] + +[sub_resource type="Animation" id="Animation_1pnxe"] +length = 0.001 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("Rope:position") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 0, +"values": [Vector2(199, 113)] +} + +[sub_resource type="Animation" id="Animation_q02ig"] +resource_name = "moving" +length = 2.0 +loop_mode = 1 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("Rope:position") +tracks/0/interp = 2 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0, 1), +"transitions": PackedFloat32Array(1, 1), +"update": 0, +"values": [Vector2(199, 113), Vector2(495, 113)] +} + +[sub_resource type="AnimationLibrary" id="AnimationLibrary_agh7j"] +_data = { +"RESET": SubResource("Animation_1pnxe"), +"moving": SubResource("Animation_q02ig") +} + +[node name="main" type="Node2D"] + +[node name="AnimationPlayer" type="AnimationPlayer" parent="."] +libraries = { +"": SubResource("AnimationLibrary_agh7j") +} +script = ExtResource("1_sodim") + +[node name="RopeAnchor" type="Marker2D" parent="."] +position = Vector2(358.629, 170.15) +script = ExtResource("2_hm10y") +rope_path = NodePath("../Rope") +rope_position = 0.322 + +[node name="Icon" type="Sprite2D" parent="RopeAnchor"] +position = Vector2(0, 32) +scale = Vector2(0.5, 0.5) +texture = ExtResource("3_y0u5w") + +[node name="RopeAnchor2" type="Marker2D" parent="."] +position = Vector2(252.384, 257.986) +rotation = 2.85458 +script = ExtResource("2_hm10y") +rope_path = NodePath("../Rope") +apply_angle = true + +[node name="Icon" type="Sprite2D" parent="RopeAnchor2"] +position = Vector2(32, 0) +rotation = -1.5708 +scale = Vector2(0.5, 0.5) +texture = ExtResource("3_y0u5w") + +[node name="Rope" type="Node2D" parent="."] +position = Vector2(199, 113) +script = ExtResource("4_jxxrc") +num_segments = 20 +rope_length = 200.0 +gravity = 39.41 +num_constraint_iterations = 15 +metadata/_edit_group_ = true + +[node name="Label" type="Label" parent="."] +offset_left = 156.0 +offset_top = 37.0 +offset_right = 514.0 +offset_bottom = 60.0 +text = "Anchors can be used to attach nodes to a rope" diff --git a/demo/rope_examples/benchmark.tscn b/demo/rope_examples/benchmark.tscn new file mode 100644 index 0000000..d45049c --- /dev/null +++ b/demo/rope_examples/benchmark.tscn @@ -0,0 +1,2017 @@ +[gd_scene load_steps=3 format=3 uid="uid://dwdcelii43tt4"] + +[ext_resource type="Script" path="res://addons/ropesim/Rope.gd" id="1_6c85y"] +[ext_resource type="Script" path="res://rope_examples/scripts/PerformanceLabel.gd" id="2_ms00a"] + +[node name="benchmark" type="Node2D"] + +[node name="PerformanceLabel" type="Label" parent="."] +offset_left = 449.0 +offset_top = -1.0 +offset_right = 509.0 +offset_bottom = 22.0 +text = "192 Ropes +2.82 ms" +script = ExtResource("2_ms00a") + +[node name="Node2D9" type="Node2D" parent="."] + +[node name="Node2D" type="Node2D" parent="Node2D9"] + +[node name="Rope" type="Node2D" parent="Node2D9/Node2D"] +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope2" type="Node2D" parent="Node2D9/Node2D"] +position = Vector2(35, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope3" type="Node2D" parent="Node2D9/Node2D"] +position = Vector2(74, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope4" type="Node2D" parent="Node2D9/Node2D"] +position = Vector2(113, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope5" type="Node2D" parent="Node2D9/Node2D"] +position = Vector2(148, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope6" type="Node2D" parent="Node2D9/Node2D"] +position = Vector2(187, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope7" type="Node2D" parent="Node2D9/Node2D"] +position = Vector2(228, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope8" type="Node2D" parent="Node2D9/Node2D"] +position = Vector2(263, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope9" type="Node2D" parent="Node2D9/Node2D"] +position = Vector2(302, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope10" type="Node2D" parent="Node2D9/Node2D"] +position = Vector2(341, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope11" type="Node2D" parent="Node2D9/Node2D"] +position = Vector2(376, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope12" type="Node2D" parent="Node2D9/Node2D"] +position = Vector2(415, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Node2D2" type="Node2D" parent="Node2D9"] +position = Vector2(0, 118) + +[node name="Rope" type="Node2D" parent="Node2D9/Node2D2"] +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope2" type="Node2D" parent="Node2D9/Node2D2"] +position = Vector2(35, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope3" type="Node2D" parent="Node2D9/Node2D2"] +position = Vector2(74, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope4" type="Node2D" parent="Node2D9/Node2D2"] +position = Vector2(113, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope5" type="Node2D" parent="Node2D9/Node2D2"] +position = Vector2(148, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope6" type="Node2D" parent="Node2D9/Node2D2"] +position = Vector2(187, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope7" type="Node2D" parent="Node2D9/Node2D2"] +position = Vector2(228, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope8" type="Node2D" parent="Node2D9/Node2D2"] +position = Vector2(263, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope9" type="Node2D" parent="Node2D9/Node2D2"] +position = Vector2(302, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope10" type="Node2D" parent="Node2D9/Node2D2"] +position = Vector2(341, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope11" type="Node2D" parent="Node2D9/Node2D2"] +position = Vector2(376, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope12" type="Node2D" parent="Node2D9/Node2D2"] +position = Vector2(415, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Node2D3" type="Node2D" parent="Node2D9"] +position = Vector2(0, 239) + +[node name="Rope" type="Node2D" parent="Node2D9/Node2D3"] +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope2" type="Node2D" parent="Node2D9/Node2D3"] +position = Vector2(35, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope3" type="Node2D" parent="Node2D9/Node2D3"] +position = Vector2(74, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope4" type="Node2D" parent="Node2D9/Node2D3"] +position = Vector2(113, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope5" type="Node2D" parent="Node2D9/Node2D3"] +position = Vector2(148, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope6" type="Node2D" parent="Node2D9/Node2D3"] +position = Vector2(187, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope7" type="Node2D" parent="Node2D9/Node2D3"] +position = Vector2(228, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope8" type="Node2D" parent="Node2D9/Node2D3"] +position = Vector2(263, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope9" type="Node2D" parent="Node2D9/Node2D3"] +position = Vector2(302, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope10" type="Node2D" parent="Node2D9/Node2D3"] +position = Vector2(341, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope11" type="Node2D" parent="Node2D9/Node2D3"] +position = Vector2(376, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope12" type="Node2D" parent="Node2D9/Node2D3"] +position = Vector2(415, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Node2D4" type="Node2D" parent="Node2D9"] +position = Vector2(0, 351) + +[node name="Rope" type="Node2D" parent="Node2D9/Node2D4"] +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope2" type="Node2D" parent="Node2D9/Node2D4"] +position = Vector2(35, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope3" type="Node2D" parent="Node2D9/Node2D4"] +position = Vector2(74, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope4" type="Node2D" parent="Node2D9/Node2D4"] +position = Vector2(113, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope5" type="Node2D" parent="Node2D9/Node2D4"] +position = Vector2(148, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope6" type="Node2D" parent="Node2D9/Node2D4"] +position = Vector2(187, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope7" type="Node2D" parent="Node2D9/Node2D4"] +position = Vector2(228, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope8" type="Node2D" parent="Node2D9/Node2D4"] +position = Vector2(263, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope9" type="Node2D" parent="Node2D9/Node2D4"] +position = Vector2(302, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope10" type="Node2D" parent="Node2D9/Node2D4"] +position = Vector2(341, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope11" type="Node2D" parent="Node2D9/Node2D4"] +position = Vector2(376, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope12" type="Node2D" parent="Node2D9/Node2D4"] +position = Vector2(415, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Node2D" type="Node2D" parent="."] + +[node name="Node2D5" type="Node2D" parent="Node2D"] +position = Vector2(20, 0) + +[node name="Rope" type="Node2D" parent="Node2D/Node2D5"] +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope2" type="Node2D" parent="Node2D/Node2D5"] +position = Vector2(35, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope3" type="Node2D" parent="Node2D/Node2D5"] +position = Vector2(74, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope4" type="Node2D" parent="Node2D/Node2D5"] +position = Vector2(113, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope5" type="Node2D" parent="Node2D/Node2D5"] +position = Vector2(148, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope6" type="Node2D" parent="Node2D/Node2D5"] +position = Vector2(187, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope7" type="Node2D" parent="Node2D/Node2D5"] +position = Vector2(228, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope8" type="Node2D" parent="Node2D/Node2D5"] +position = Vector2(263, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope9" type="Node2D" parent="Node2D/Node2D5"] +position = Vector2(302, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope10" type="Node2D" parent="Node2D/Node2D5"] +position = Vector2(341, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope11" type="Node2D" parent="Node2D/Node2D5"] +position = Vector2(376, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope12" type="Node2D" parent="Node2D/Node2D5"] +position = Vector2(415, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Node2D6" type="Node2D" parent="Node2D"] +position = Vector2(20, 118) + +[node name="Rope" type="Node2D" parent="Node2D/Node2D6"] +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope2" type="Node2D" parent="Node2D/Node2D6"] +position = Vector2(35, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope3" type="Node2D" parent="Node2D/Node2D6"] +position = Vector2(74, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope4" type="Node2D" parent="Node2D/Node2D6"] +position = Vector2(113, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope5" type="Node2D" parent="Node2D/Node2D6"] +position = Vector2(148, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope6" type="Node2D" parent="Node2D/Node2D6"] +position = Vector2(187, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope7" type="Node2D" parent="Node2D/Node2D6"] +position = Vector2(228, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope8" type="Node2D" parent="Node2D/Node2D6"] +position = Vector2(263, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope9" type="Node2D" parent="Node2D/Node2D6"] +position = Vector2(302, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope10" type="Node2D" parent="Node2D/Node2D6"] +position = Vector2(341, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope11" type="Node2D" parent="Node2D/Node2D6"] +position = Vector2(376, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope12" type="Node2D" parent="Node2D/Node2D6"] +position = Vector2(415, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Node2D7" type="Node2D" parent="Node2D"] +position = Vector2(20, 239) + +[node name="Rope" type="Node2D" parent="Node2D/Node2D7"] +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope2" type="Node2D" parent="Node2D/Node2D7"] +position = Vector2(35, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope3" type="Node2D" parent="Node2D/Node2D7"] +position = Vector2(74, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope4" type="Node2D" parent="Node2D/Node2D7"] +position = Vector2(113, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope5" type="Node2D" parent="Node2D/Node2D7"] +position = Vector2(148, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope6" type="Node2D" parent="Node2D/Node2D7"] +position = Vector2(187, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope7" type="Node2D" parent="Node2D/Node2D7"] +position = Vector2(228, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope8" type="Node2D" parent="Node2D/Node2D7"] +position = Vector2(263, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope9" type="Node2D" parent="Node2D/Node2D7"] +position = Vector2(302, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope10" type="Node2D" parent="Node2D/Node2D7"] +position = Vector2(341, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope11" type="Node2D" parent="Node2D/Node2D7"] +position = Vector2(376, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope12" type="Node2D" parent="Node2D/Node2D7"] +position = Vector2(415, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Node2D8" type="Node2D" parent="Node2D"] +position = Vector2(20, 351) + +[node name="Rope" type="Node2D" parent="Node2D/Node2D8"] +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope2" type="Node2D" parent="Node2D/Node2D8"] +position = Vector2(35, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope3" type="Node2D" parent="Node2D/Node2D8"] +position = Vector2(74, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope4" type="Node2D" parent="Node2D/Node2D8"] +position = Vector2(113, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope5" type="Node2D" parent="Node2D/Node2D8"] +position = Vector2(148, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope6" type="Node2D" parent="Node2D/Node2D8"] +position = Vector2(187, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope7" type="Node2D" parent="Node2D/Node2D8"] +position = Vector2(228, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope8" type="Node2D" parent="Node2D/Node2D8"] +position = Vector2(263, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope9" type="Node2D" parent="Node2D/Node2D8"] +position = Vector2(302, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope10" type="Node2D" parent="Node2D/Node2D8"] +position = Vector2(341, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope11" type="Node2D" parent="Node2D/Node2D8"] +position = Vector2(376, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope12" type="Node2D" parent="Node2D/Node2D8"] +position = Vector2(415, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Node2D10" type="Node2D" parent="."] +position = Vector2(-11, -78) + +[node name="Node2D" type="Node2D" parent="Node2D10"] + +[node name="Rope" type="Node2D" parent="Node2D10/Node2D"] +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope2" type="Node2D" parent="Node2D10/Node2D"] +position = Vector2(35, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope3" type="Node2D" parent="Node2D10/Node2D"] +position = Vector2(74, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope4" type="Node2D" parent="Node2D10/Node2D"] +position = Vector2(113, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope5" type="Node2D" parent="Node2D10/Node2D"] +position = Vector2(148, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope6" type="Node2D" parent="Node2D10/Node2D"] +position = Vector2(187, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope7" type="Node2D" parent="Node2D10/Node2D"] +position = Vector2(228, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope8" type="Node2D" parent="Node2D10/Node2D"] +position = Vector2(263, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope9" type="Node2D" parent="Node2D10/Node2D"] +position = Vector2(302, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope10" type="Node2D" parent="Node2D10/Node2D"] +position = Vector2(341, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope11" type="Node2D" parent="Node2D10/Node2D"] +position = Vector2(376, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope12" type="Node2D" parent="Node2D10/Node2D"] +position = Vector2(415, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Node2D2" type="Node2D" parent="Node2D10"] +position = Vector2(0, 118) + +[node name="Rope" type="Node2D" parent="Node2D10/Node2D2"] +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope2" type="Node2D" parent="Node2D10/Node2D2"] +position = Vector2(35, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope3" type="Node2D" parent="Node2D10/Node2D2"] +position = Vector2(74, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope4" type="Node2D" parent="Node2D10/Node2D2"] +position = Vector2(113, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope5" type="Node2D" parent="Node2D10/Node2D2"] +position = Vector2(148, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope6" type="Node2D" parent="Node2D10/Node2D2"] +position = Vector2(187, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope7" type="Node2D" parent="Node2D10/Node2D2"] +position = Vector2(228, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope8" type="Node2D" parent="Node2D10/Node2D2"] +position = Vector2(263, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope9" type="Node2D" parent="Node2D10/Node2D2"] +position = Vector2(302, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope10" type="Node2D" parent="Node2D10/Node2D2"] +position = Vector2(341, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope11" type="Node2D" parent="Node2D10/Node2D2"] +position = Vector2(376, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope12" type="Node2D" parent="Node2D10/Node2D2"] +position = Vector2(415, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Node2D3" type="Node2D" parent="Node2D10"] +position = Vector2(0, 239) + +[node name="Rope" type="Node2D" parent="Node2D10/Node2D3"] +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope2" type="Node2D" parent="Node2D10/Node2D3"] +position = Vector2(35, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope3" type="Node2D" parent="Node2D10/Node2D3"] +position = Vector2(74, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope4" type="Node2D" parent="Node2D10/Node2D3"] +position = Vector2(113, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope5" type="Node2D" parent="Node2D10/Node2D3"] +position = Vector2(148, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope6" type="Node2D" parent="Node2D10/Node2D3"] +position = Vector2(187, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope7" type="Node2D" parent="Node2D10/Node2D3"] +position = Vector2(228, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope8" type="Node2D" parent="Node2D10/Node2D3"] +position = Vector2(263, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope9" type="Node2D" parent="Node2D10/Node2D3"] +position = Vector2(302, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope10" type="Node2D" parent="Node2D10/Node2D3"] +position = Vector2(341, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope11" type="Node2D" parent="Node2D10/Node2D3"] +position = Vector2(376, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope12" type="Node2D" parent="Node2D10/Node2D3"] +position = Vector2(415, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Node2D4" type="Node2D" parent="Node2D10"] +position = Vector2(0, 351) + +[node name="Rope" type="Node2D" parent="Node2D10/Node2D4"] +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope2" type="Node2D" parent="Node2D10/Node2D4"] +position = Vector2(35, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope3" type="Node2D" parent="Node2D10/Node2D4"] +position = Vector2(74, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope4" type="Node2D" parent="Node2D10/Node2D4"] +position = Vector2(113, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope5" type="Node2D" parent="Node2D10/Node2D4"] +position = Vector2(148, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope6" type="Node2D" parent="Node2D10/Node2D4"] +position = Vector2(187, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope7" type="Node2D" parent="Node2D10/Node2D4"] +position = Vector2(228, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope8" type="Node2D" parent="Node2D10/Node2D4"] +position = Vector2(263, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope9" type="Node2D" parent="Node2D10/Node2D4"] +position = Vector2(302, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope10" type="Node2D" parent="Node2D10/Node2D4"] +position = Vector2(341, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope11" type="Node2D" parent="Node2D10/Node2D4"] +position = Vector2(376, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope12" type="Node2D" parent="Node2D10/Node2D4"] +position = Vector2(415, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Node2D2" type="Node2D" parent="."] +position = Vector2(-11, -78) + +[node name="Node2D5" type="Node2D" parent="Node2D2"] +position = Vector2(20, 0) + +[node name="Rope" type="Node2D" parent="Node2D2/Node2D5"] +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope2" type="Node2D" parent="Node2D2/Node2D5"] +position = Vector2(35, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope3" type="Node2D" parent="Node2D2/Node2D5"] +position = Vector2(74, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope4" type="Node2D" parent="Node2D2/Node2D5"] +position = Vector2(113, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope5" type="Node2D" parent="Node2D2/Node2D5"] +position = Vector2(148, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope6" type="Node2D" parent="Node2D2/Node2D5"] +position = Vector2(187, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope7" type="Node2D" parent="Node2D2/Node2D5"] +position = Vector2(228, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope8" type="Node2D" parent="Node2D2/Node2D5"] +position = Vector2(263, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope9" type="Node2D" parent="Node2D2/Node2D5"] +position = Vector2(302, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope10" type="Node2D" parent="Node2D2/Node2D5"] +position = Vector2(341, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope11" type="Node2D" parent="Node2D2/Node2D5"] +position = Vector2(376, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope12" type="Node2D" parent="Node2D2/Node2D5"] +position = Vector2(415, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Node2D6" type="Node2D" parent="Node2D2"] +position = Vector2(20, 118) + +[node name="Rope" type="Node2D" parent="Node2D2/Node2D6"] +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope2" type="Node2D" parent="Node2D2/Node2D6"] +position = Vector2(35, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope3" type="Node2D" parent="Node2D2/Node2D6"] +position = Vector2(74, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope4" type="Node2D" parent="Node2D2/Node2D6"] +position = Vector2(113, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope5" type="Node2D" parent="Node2D2/Node2D6"] +position = Vector2(148, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope6" type="Node2D" parent="Node2D2/Node2D6"] +position = Vector2(187, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope7" type="Node2D" parent="Node2D2/Node2D6"] +position = Vector2(228, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope8" type="Node2D" parent="Node2D2/Node2D6"] +position = Vector2(263, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope9" type="Node2D" parent="Node2D2/Node2D6"] +position = Vector2(302, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope10" type="Node2D" parent="Node2D2/Node2D6"] +position = Vector2(341, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope11" type="Node2D" parent="Node2D2/Node2D6"] +position = Vector2(376, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope12" type="Node2D" parent="Node2D2/Node2D6"] +position = Vector2(415, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Node2D7" type="Node2D" parent="Node2D2"] +position = Vector2(20, 239) + +[node name="Rope" type="Node2D" parent="Node2D2/Node2D7"] +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope2" type="Node2D" parent="Node2D2/Node2D7"] +position = Vector2(35, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope3" type="Node2D" parent="Node2D2/Node2D7"] +position = Vector2(74, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope4" type="Node2D" parent="Node2D2/Node2D7"] +position = Vector2(113, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope5" type="Node2D" parent="Node2D2/Node2D7"] +position = Vector2(148, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope6" type="Node2D" parent="Node2D2/Node2D7"] +position = Vector2(187, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope7" type="Node2D" parent="Node2D2/Node2D7"] +position = Vector2(228, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope8" type="Node2D" parent="Node2D2/Node2D7"] +position = Vector2(263, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope9" type="Node2D" parent="Node2D2/Node2D7"] +position = Vector2(302, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope10" type="Node2D" parent="Node2D2/Node2D7"] +position = Vector2(341, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope11" type="Node2D" parent="Node2D2/Node2D7"] +position = Vector2(376, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope12" type="Node2D" parent="Node2D2/Node2D7"] +position = Vector2(415, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Node2D8" type="Node2D" parent="Node2D2"] +position = Vector2(20, 351) + +[node name="Rope" type="Node2D" parent="Node2D2/Node2D8"] +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope2" type="Node2D" parent="Node2D2/Node2D8"] +position = Vector2(35, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope3" type="Node2D" parent="Node2D2/Node2D8"] +position = Vector2(74, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope4" type="Node2D" parent="Node2D2/Node2D8"] +position = Vector2(113, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope5" type="Node2D" parent="Node2D2/Node2D8"] +position = Vector2(148, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope6" type="Node2D" parent="Node2D2/Node2D8"] +position = Vector2(187, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope7" type="Node2D" parent="Node2D2/Node2D8"] +position = Vector2(228, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope8" type="Node2D" parent="Node2D2/Node2D8"] +position = Vector2(263, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope9" type="Node2D" parent="Node2D2/Node2D8"] +position = Vector2(302, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope10" type="Node2D" parent="Node2D2/Node2D8"] +position = Vector2(341, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope11" type="Node2D" parent="Node2D2/Node2D8"] +position = Vector2(376, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope12" type="Node2D" parent="Node2D2/Node2D8"] +position = Vector2(415, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Node2D11" type="Node2D" parent="."] +position = Vector2(623, 8) + +[node name="Node2D" type="Node2D" parent="Node2D11"] + +[node name="Rope" type="Node2D" parent="Node2D11/Node2D"] +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope2" type="Node2D" parent="Node2D11/Node2D"] +position = Vector2(35, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope3" type="Node2D" parent="Node2D11/Node2D"] +position = Vector2(74, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope4" type="Node2D" parent="Node2D11/Node2D"] +position = Vector2(113, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope5" type="Node2D" parent="Node2D11/Node2D"] +position = Vector2(148, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope6" type="Node2D" parent="Node2D11/Node2D"] +position = Vector2(187, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope7" type="Node2D" parent="Node2D11/Node2D"] +position = Vector2(228, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope8" type="Node2D" parent="Node2D11/Node2D"] +position = Vector2(263, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope9" type="Node2D" parent="Node2D11/Node2D"] +position = Vector2(302, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope10" type="Node2D" parent="Node2D11/Node2D"] +position = Vector2(341, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope11" type="Node2D" parent="Node2D11/Node2D"] +position = Vector2(376, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope12" type="Node2D" parent="Node2D11/Node2D"] +position = Vector2(415, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Node2D2" type="Node2D" parent="Node2D11"] +position = Vector2(0, 118) + +[node name="Rope" type="Node2D" parent="Node2D11/Node2D2"] +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope2" type="Node2D" parent="Node2D11/Node2D2"] +position = Vector2(35, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope3" type="Node2D" parent="Node2D11/Node2D2"] +position = Vector2(74, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope4" type="Node2D" parent="Node2D11/Node2D2"] +position = Vector2(113, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope5" type="Node2D" parent="Node2D11/Node2D2"] +position = Vector2(148, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope6" type="Node2D" parent="Node2D11/Node2D2"] +position = Vector2(187, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope7" type="Node2D" parent="Node2D11/Node2D2"] +position = Vector2(228, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope8" type="Node2D" parent="Node2D11/Node2D2"] +position = Vector2(263, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope9" type="Node2D" parent="Node2D11/Node2D2"] +position = Vector2(302, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope10" type="Node2D" parent="Node2D11/Node2D2"] +position = Vector2(341, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope11" type="Node2D" parent="Node2D11/Node2D2"] +position = Vector2(376, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope12" type="Node2D" parent="Node2D11/Node2D2"] +position = Vector2(415, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Node2D3" type="Node2D" parent="Node2D11"] +position = Vector2(0, 239) + +[node name="Rope" type="Node2D" parent="Node2D11/Node2D3"] +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope2" type="Node2D" parent="Node2D11/Node2D3"] +position = Vector2(35, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope3" type="Node2D" parent="Node2D11/Node2D3"] +position = Vector2(74, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope4" type="Node2D" parent="Node2D11/Node2D3"] +position = Vector2(113, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope5" type="Node2D" parent="Node2D11/Node2D3"] +position = Vector2(148, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope6" type="Node2D" parent="Node2D11/Node2D3"] +position = Vector2(187, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope7" type="Node2D" parent="Node2D11/Node2D3"] +position = Vector2(228, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope8" type="Node2D" parent="Node2D11/Node2D3"] +position = Vector2(263, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope9" type="Node2D" parent="Node2D11/Node2D3"] +position = Vector2(302, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope10" type="Node2D" parent="Node2D11/Node2D3"] +position = Vector2(341, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope11" type="Node2D" parent="Node2D11/Node2D3"] +position = Vector2(376, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope12" type="Node2D" parent="Node2D11/Node2D3"] +position = Vector2(415, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Node2D4" type="Node2D" parent="Node2D11"] +position = Vector2(0, 351) + +[node name="Rope" type="Node2D" parent="Node2D11/Node2D4"] +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope2" type="Node2D" parent="Node2D11/Node2D4"] +position = Vector2(35, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope3" type="Node2D" parent="Node2D11/Node2D4"] +position = Vector2(74, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope4" type="Node2D" parent="Node2D11/Node2D4"] +position = Vector2(113, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope5" type="Node2D" parent="Node2D11/Node2D4"] +position = Vector2(148, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope6" type="Node2D" parent="Node2D11/Node2D4"] +position = Vector2(187, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope7" type="Node2D" parent="Node2D11/Node2D4"] +position = Vector2(228, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope8" type="Node2D" parent="Node2D11/Node2D4"] +position = Vector2(263, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope9" type="Node2D" parent="Node2D11/Node2D4"] +position = Vector2(302, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope10" type="Node2D" parent="Node2D11/Node2D4"] +position = Vector2(341, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope11" type="Node2D" parent="Node2D11/Node2D4"] +position = Vector2(376, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope12" type="Node2D" parent="Node2D11/Node2D4"] +position = Vector2(415, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Node2D3" type="Node2D" parent="."] +position = Vector2(623, 8) + +[node name="Node2D5" type="Node2D" parent="Node2D3"] +position = Vector2(20, 0) + +[node name="Rope" type="Node2D" parent="Node2D3/Node2D5"] +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope2" type="Node2D" parent="Node2D3/Node2D5"] +position = Vector2(35, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope3" type="Node2D" parent="Node2D3/Node2D5"] +position = Vector2(74, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope4" type="Node2D" parent="Node2D3/Node2D5"] +position = Vector2(113, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope5" type="Node2D" parent="Node2D3/Node2D5"] +position = Vector2(148, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope6" type="Node2D" parent="Node2D3/Node2D5"] +position = Vector2(187, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope7" type="Node2D" parent="Node2D3/Node2D5"] +position = Vector2(228, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope8" type="Node2D" parent="Node2D3/Node2D5"] +position = Vector2(263, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope9" type="Node2D" parent="Node2D3/Node2D5"] +position = Vector2(302, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope10" type="Node2D" parent="Node2D3/Node2D5"] +position = Vector2(341, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope11" type="Node2D" parent="Node2D3/Node2D5"] +position = Vector2(376, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope12" type="Node2D" parent="Node2D3/Node2D5"] +position = Vector2(415, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Node2D6" type="Node2D" parent="Node2D3"] +position = Vector2(20, 118) + +[node name="Rope" type="Node2D" parent="Node2D3/Node2D6"] +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope2" type="Node2D" parent="Node2D3/Node2D6"] +position = Vector2(35, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope3" type="Node2D" parent="Node2D3/Node2D6"] +position = Vector2(74, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope4" type="Node2D" parent="Node2D3/Node2D6"] +position = Vector2(113, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope5" type="Node2D" parent="Node2D3/Node2D6"] +position = Vector2(148, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope6" type="Node2D" parent="Node2D3/Node2D6"] +position = Vector2(187, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope7" type="Node2D" parent="Node2D3/Node2D6"] +position = Vector2(228, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope8" type="Node2D" parent="Node2D3/Node2D6"] +position = Vector2(263, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope9" type="Node2D" parent="Node2D3/Node2D6"] +position = Vector2(302, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope10" type="Node2D" parent="Node2D3/Node2D6"] +position = Vector2(341, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope11" type="Node2D" parent="Node2D3/Node2D6"] +position = Vector2(376, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope12" type="Node2D" parent="Node2D3/Node2D6"] +position = Vector2(415, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Node2D7" type="Node2D" parent="Node2D3"] +position = Vector2(20, 239) + +[node name="Rope" type="Node2D" parent="Node2D3/Node2D7"] +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope2" type="Node2D" parent="Node2D3/Node2D7"] +position = Vector2(35, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope3" type="Node2D" parent="Node2D3/Node2D7"] +position = Vector2(74, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope4" type="Node2D" parent="Node2D3/Node2D7"] +position = Vector2(113, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope5" type="Node2D" parent="Node2D3/Node2D7"] +position = Vector2(148, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope6" type="Node2D" parent="Node2D3/Node2D7"] +position = Vector2(187, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope7" type="Node2D" parent="Node2D3/Node2D7"] +position = Vector2(228, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope8" type="Node2D" parent="Node2D3/Node2D7"] +position = Vector2(263, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope9" type="Node2D" parent="Node2D3/Node2D7"] +position = Vector2(302, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope10" type="Node2D" parent="Node2D3/Node2D7"] +position = Vector2(341, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope11" type="Node2D" parent="Node2D3/Node2D7"] +position = Vector2(376, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope12" type="Node2D" parent="Node2D3/Node2D7"] +position = Vector2(415, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Node2D8" type="Node2D" parent="Node2D3"] +position = Vector2(20, 351) + +[node name="Rope" type="Node2D" parent="Node2D3/Node2D8"] +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope2" type="Node2D" parent="Node2D3/Node2D8"] +position = Vector2(35, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope3" type="Node2D" parent="Node2D3/Node2D8"] +position = Vector2(74, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope4" type="Node2D" parent="Node2D3/Node2D8"] +position = Vector2(113, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope5" type="Node2D" parent="Node2D3/Node2D8"] +position = Vector2(148, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope6" type="Node2D" parent="Node2D3/Node2D8"] +position = Vector2(187, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope7" type="Node2D" parent="Node2D3/Node2D8"] +position = Vector2(228, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope8" type="Node2D" parent="Node2D3/Node2D8"] +position = Vector2(263, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope9" type="Node2D" parent="Node2D3/Node2D8"] +position = Vector2(302, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope10" type="Node2D" parent="Node2D3/Node2D8"] +position = Vector2(341, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope11" type="Node2D" parent="Node2D3/Node2D8"] +position = Vector2(376, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope12" type="Node2D" parent="Node2D3/Node2D8"] +position = Vector2(415, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Node2D12" type="Node2D" parent="."] +position = Vector2(612, -70) + +[node name="Node2D" type="Node2D" parent="Node2D12"] + +[node name="Rope" type="Node2D" parent="Node2D12/Node2D"] +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope2" type="Node2D" parent="Node2D12/Node2D"] +position = Vector2(35, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope3" type="Node2D" parent="Node2D12/Node2D"] +position = Vector2(74, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope4" type="Node2D" parent="Node2D12/Node2D"] +position = Vector2(113, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope5" type="Node2D" parent="Node2D12/Node2D"] +position = Vector2(148, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope6" type="Node2D" parent="Node2D12/Node2D"] +position = Vector2(187, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope7" type="Node2D" parent="Node2D12/Node2D"] +position = Vector2(228, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope8" type="Node2D" parent="Node2D12/Node2D"] +position = Vector2(263, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope9" type="Node2D" parent="Node2D12/Node2D"] +position = Vector2(302, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope10" type="Node2D" parent="Node2D12/Node2D"] +position = Vector2(341, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope11" type="Node2D" parent="Node2D12/Node2D"] +position = Vector2(376, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope12" type="Node2D" parent="Node2D12/Node2D"] +position = Vector2(415, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Node2D2" type="Node2D" parent="Node2D12"] +position = Vector2(0, 118) + +[node name="Rope" type="Node2D" parent="Node2D12/Node2D2"] +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope2" type="Node2D" parent="Node2D12/Node2D2"] +position = Vector2(35, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope3" type="Node2D" parent="Node2D12/Node2D2"] +position = Vector2(74, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope4" type="Node2D" parent="Node2D12/Node2D2"] +position = Vector2(113, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope5" type="Node2D" parent="Node2D12/Node2D2"] +position = Vector2(148, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope6" type="Node2D" parent="Node2D12/Node2D2"] +position = Vector2(187, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope7" type="Node2D" parent="Node2D12/Node2D2"] +position = Vector2(228, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope8" type="Node2D" parent="Node2D12/Node2D2"] +position = Vector2(263, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope9" type="Node2D" parent="Node2D12/Node2D2"] +position = Vector2(302, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope10" type="Node2D" parent="Node2D12/Node2D2"] +position = Vector2(341, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope11" type="Node2D" parent="Node2D12/Node2D2"] +position = Vector2(376, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope12" type="Node2D" parent="Node2D12/Node2D2"] +position = Vector2(415, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Node2D3" type="Node2D" parent="Node2D12"] +position = Vector2(0, 239) + +[node name="Rope" type="Node2D" parent="Node2D12/Node2D3"] +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope2" type="Node2D" parent="Node2D12/Node2D3"] +position = Vector2(35, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope3" type="Node2D" parent="Node2D12/Node2D3"] +position = Vector2(74, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope4" type="Node2D" parent="Node2D12/Node2D3"] +position = Vector2(113, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope5" type="Node2D" parent="Node2D12/Node2D3"] +position = Vector2(148, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope6" type="Node2D" parent="Node2D12/Node2D3"] +position = Vector2(187, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope7" type="Node2D" parent="Node2D12/Node2D3"] +position = Vector2(228, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope8" type="Node2D" parent="Node2D12/Node2D3"] +position = Vector2(263, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope9" type="Node2D" parent="Node2D12/Node2D3"] +position = Vector2(302, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope10" type="Node2D" parent="Node2D12/Node2D3"] +position = Vector2(341, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope11" type="Node2D" parent="Node2D12/Node2D3"] +position = Vector2(376, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope12" type="Node2D" parent="Node2D12/Node2D3"] +position = Vector2(415, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Node2D4" type="Node2D" parent="Node2D12"] +position = Vector2(0, 351) + +[node name="Rope" type="Node2D" parent="Node2D12/Node2D4"] +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope2" type="Node2D" parent="Node2D12/Node2D4"] +position = Vector2(35, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope3" type="Node2D" parent="Node2D12/Node2D4"] +position = Vector2(74, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope4" type="Node2D" parent="Node2D12/Node2D4"] +position = Vector2(113, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope5" type="Node2D" parent="Node2D12/Node2D4"] +position = Vector2(148, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope6" type="Node2D" parent="Node2D12/Node2D4"] +position = Vector2(187, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope7" type="Node2D" parent="Node2D12/Node2D4"] +position = Vector2(228, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope8" type="Node2D" parent="Node2D12/Node2D4"] +position = Vector2(263, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope9" type="Node2D" parent="Node2D12/Node2D4"] +position = Vector2(302, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope10" type="Node2D" parent="Node2D12/Node2D4"] +position = Vector2(341, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope11" type="Node2D" parent="Node2D12/Node2D4"] +position = Vector2(376, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope12" type="Node2D" parent="Node2D12/Node2D4"] +position = Vector2(415, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Node2D4" type="Node2D" parent="."] +position = Vector2(612, -70) + +[node name="Node2D5" type="Node2D" parent="Node2D4"] +position = Vector2(20, 0) + +[node name="Rope" type="Node2D" parent="Node2D4/Node2D5"] +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope2" type="Node2D" parent="Node2D4/Node2D5"] +position = Vector2(35, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope3" type="Node2D" parent="Node2D4/Node2D5"] +position = Vector2(74, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope4" type="Node2D" parent="Node2D4/Node2D5"] +position = Vector2(113, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope5" type="Node2D" parent="Node2D4/Node2D5"] +position = Vector2(148, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope6" type="Node2D" parent="Node2D4/Node2D5"] +position = Vector2(187, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope7" type="Node2D" parent="Node2D4/Node2D5"] +position = Vector2(228, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope8" type="Node2D" parent="Node2D4/Node2D5"] +position = Vector2(263, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope9" type="Node2D" parent="Node2D4/Node2D5"] +position = Vector2(302, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope10" type="Node2D" parent="Node2D4/Node2D5"] +position = Vector2(341, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope11" type="Node2D" parent="Node2D4/Node2D5"] +position = Vector2(376, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope12" type="Node2D" parent="Node2D4/Node2D5"] +position = Vector2(415, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Node2D6" type="Node2D" parent="Node2D4"] +position = Vector2(20, 118) + +[node name="Rope" type="Node2D" parent="Node2D4/Node2D6"] +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope2" type="Node2D" parent="Node2D4/Node2D6"] +position = Vector2(35, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope3" type="Node2D" parent="Node2D4/Node2D6"] +position = Vector2(74, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope4" type="Node2D" parent="Node2D4/Node2D6"] +position = Vector2(113, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope5" type="Node2D" parent="Node2D4/Node2D6"] +position = Vector2(148, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope6" type="Node2D" parent="Node2D4/Node2D6"] +position = Vector2(187, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope7" type="Node2D" parent="Node2D4/Node2D6"] +position = Vector2(228, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope8" type="Node2D" parent="Node2D4/Node2D6"] +position = Vector2(263, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope9" type="Node2D" parent="Node2D4/Node2D6"] +position = Vector2(302, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope10" type="Node2D" parent="Node2D4/Node2D6"] +position = Vector2(341, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope11" type="Node2D" parent="Node2D4/Node2D6"] +position = Vector2(376, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope12" type="Node2D" parent="Node2D4/Node2D6"] +position = Vector2(415, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Node2D7" type="Node2D" parent="Node2D4"] +position = Vector2(20, 239) + +[node name="Rope" type="Node2D" parent="Node2D4/Node2D7"] +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope2" type="Node2D" parent="Node2D4/Node2D7"] +position = Vector2(35, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope3" type="Node2D" parent="Node2D4/Node2D7"] +position = Vector2(74, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope4" type="Node2D" parent="Node2D4/Node2D7"] +position = Vector2(113, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope5" type="Node2D" parent="Node2D4/Node2D7"] +position = Vector2(148, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope6" type="Node2D" parent="Node2D4/Node2D7"] +position = Vector2(187, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope7" type="Node2D" parent="Node2D4/Node2D7"] +position = Vector2(228, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope8" type="Node2D" parent="Node2D4/Node2D7"] +position = Vector2(263, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope9" type="Node2D" parent="Node2D4/Node2D7"] +position = Vector2(302, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope10" type="Node2D" parent="Node2D4/Node2D7"] +position = Vector2(341, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope11" type="Node2D" parent="Node2D4/Node2D7"] +position = Vector2(376, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope12" type="Node2D" parent="Node2D4/Node2D7"] +position = Vector2(415, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Node2D8" type="Node2D" parent="Node2D4"] +position = Vector2(20, 351) + +[node name="Rope" type="Node2D" parent="Node2D4/Node2D8"] +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope2" type="Node2D" parent="Node2D4/Node2D8"] +position = Vector2(35, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope3" type="Node2D" parent="Node2D4/Node2D8"] +position = Vector2(74, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope4" type="Node2D" parent="Node2D4/Node2D8"] +position = Vector2(113, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope5" type="Node2D" parent="Node2D4/Node2D8"] +position = Vector2(148, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope6" type="Node2D" parent="Node2D4/Node2D8"] +position = Vector2(187, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope7" type="Node2D" parent="Node2D4/Node2D8"] +position = Vector2(228, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope8" type="Node2D" parent="Node2D4/Node2D8"] +position = Vector2(263, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope9" type="Node2D" parent="Node2D4/Node2D8"] +position = Vector2(302, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope10" type="Node2D" parent="Node2D4/Node2D8"] +position = Vector2(341, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope11" type="Node2D" parent="Node2D4/Node2D8"] +position = Vector2(376, 0) +script = ExtResource("1_6c85y") +num_segments = 30 + +[node name="Rope12" type="Node2D" parent="Node2D4/Node2D8"] +position = Vector2(415, 0) +script = ExtResource("1_6c85y") +num_segments = 30 diff --git a/demo/rope_examples/collision_detection.tscn b/demo/rope_examples/collision_detection.tscn new file mode 100644 index 0000000..fd8beea --- /dev/null +++ b/demo/rope_examples/collision_detection.tscn @@ -0,0 +1,101 @@ +[gd_scene load_steps=10 format=3 uid="uid://d3hb4pwl1ucly"] + +[ext_resource type="Script" path="res://rope_examples/scripts/animation_player.gd" id="1_auvbo"] +[ext_resource type="Script" path="res://addons/ropesim/RopeCollisionShapeGenerator.gd" id="3_10pwp"] +[ext_resource type="Texture2D" uid="uid://criwv6nuivcxy" path="res://rope_examples/icon.svg" id="3_jqc61"] +[ext_resource type="Script" path="res://rope_examples/scripts/collision_detector.gd" id="4_b0a70"] +[ext_resource type="Script" path="res://addons/ropesim/Rope.gd" id="4_wgvfy"] + +[sub_resource type="Animation" id="Animation_1pnxe"] +length = 0.001 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("Rope:position") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 0, +"values": [Vector2(199, 113)] +} + +[sub_resource type="Animation" id="Animation_q02ig"] +resource_name = "moving" +length = 2.0 +loop_mode = 1 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("Rope:position") +tracks/0/interp = 2 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0, 1), +"transitions": PackedFloat32Array(1, 1), +"update": 0, +"values": [Vector2(199, 200), Vector2(300, 200)] +} + +[sub_resource type="AnimationLibrary" id="AnimationLibrary_agh7j"] +_data = { +"RESET": SubResource("Animation_1pnxe"), +"moving": SubResource("Animation_q02ig") +} + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_xyd70"] +size = Vector2(128, 128) + +[node name="main" type="Node2D"] + +[node name="AnimationPlayer" type="AnimationPlayer" parent="."] +libraries = { +"": SubResource("AnimationLibrary_agh7j") +} +script = ExtResource("1_auvbo") + +[node name="Label" type="Label" parent="."] +offset_left = 18.0 +offset_top = 6.0 +offset_right = 937.0 +offset_bottom = 81.0 +text = "Place RopeCollisionShapeGenerator below a collision object, i.e. Area2D, CharacterBody2D, StaticBody2D, RigidBody2D. +It will generate corresponding CollisionShapes using SegmentShape2D shapes. +Other shapes are not supported at the moment. + +NOTE: Collisions don't work in editor. Run this scene to see the example work." + +[node name="Rope" type="Node2D" parent="."] +position = Vector2(199, 113) +script = ExtResource("4_wgvfy") +num_segments = 20 +rope_length = 200.0 +gravity = 25.0 +metadata/_edit_group_ = true + +[node name="Area2D" type="Area2D" parent="."] +position = Vector2(19, 114) +script = ExtResource("4_b0a70") + +[node name="RopeCollisionShapeGenerator" type="Node" parent="Area2D"] +script = ExtResource("3_10pwp") +rope_path = NodePath("../../Rope") + +[node name="indicator" type="Label" parent="Area2D"] +visible = false +offset_left = 411.0 +offset_top = 142.0 +offset_right = 481.0 +offset_bottom = 165.0 +text = "Collision!" +metadata/_edit_use_anchors_ = true + +[node name="CharacterBody2D" type="CharacterBody2D" parent="."] +position = Vector2(133, 400) + +[node name="Icon" type="Sprite2D" parent="CharacterBody2D"] +texture = ExtResource("3_jqc61") + +[node name="CollisionShape2D" type="CollisionShape2D" parent="CharacterBody2D"] +shape = SubResource("RectangleShape2D_xyd70") diff --git a/demo/rope_examples/handles.tscn b/demo/rope_examples/handles.tscn new file mode 100644 index 0000000..ad77ead --- /dev/null +++ b/demo/rope_examples/handles.tscn @@ -0,0 +1,189 @@ +[gd_scene load_steps=7 format=3 uid="uid://bajrq5fmp5squ"] + +[ext_resource type="Script" path="res://addons/ropesim/RopeHandle.gd" id="1_n2oah"] +[ext_resource type="Script" path="res://rope_examples/scripts/animation_player.gd" id="3_g6662"] +[ext_resource type="Script" path="res://addons/ropesim/Rope.gd" id="3_h0p3k"] + +[sub_resource type="Animation" id="Animation_1pnxe"] +length = 0.001 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("RopeHandle3:position") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 0, +"values": [Vector2(873, 301)] +} + +[sub_resource type="Animation" id="Animation_q02ig"] +resource_name = "moving" +length = 2.0 +loop_mode = 1 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("RopeHandle3:position") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0, 0.5, 1, 1.5), +"transitions": PackedFloat32Array(1, 1, 1, 1), +"update": 0, +"values": [Vector2(833, 334), Vector2(842, 276), Vector2(919, 283), Vector2(863, 350)] +} + +[sub_resource type="AnimationLibrary" id="AnimationLibrary_agh7j"] +_data = { +"RESET": SubResource("Animation_1pnxe"), +"moving": SubResource("Animation_q02ig") +} + +[node name="main" type="Node2D"] + +[node name="Label" type="Label" parent="."] +offset_left = 31.0 +offset_top = 15.0 +offset_right = 720.0 +offset_bottom = 64.0 +text = "Handles can be used to control parts of the rope. +The \"strength\" property determines how strong a point will be constrained to the handle." + +[node name="Label2" type="Label" parent="."] +offset_left = 108.0 +offset_top = 128.0 +offset_right = 214.0 +offset_bottom = 151.0 +text = "strength = 0.0" + +[node name="Rope" type="Node2D" parent="."] +position = Vector2(96, 191) +script = ExtResource("3_h0p3k") +num_segments = 20 +rope_length = 200.0 +metadata/_edit_group_ = true + +[node name="RopeHandle" type="Marker2D" parent="."] +position = Vector2(206, 165) +script = ExtResource("1_n2oah") +rope_path = NodePath("../Rope") +rope_position = 0.582 + +[node name="Label4" type="Label" parent="."] +offset_left = 319.0 +offset_top = 123.0 +offset_right = 425.0 +offset_bottom = 146.0 +text = "strength = 1.0" + +[node name="Rope3" type="Node2D" parent="."] +position = Vector2(311, 187) +script = ExtResource("3_h0p3k") +num_segments = 20 +rope_length = 200.0 +metadata/_edit_group_ = true + +[node name="RopeHandle2" type="Marker2D" parent="."] +position = Vector2(417, 160) +script = ExtResource("1_n2oah") +rope_path = NodePath("../Rope3") +rope_position = 0.582 +strength = 1.0 + +[node name="Label3" type="Label" parent="."] +offset_left = 31.0 +offset_top = 340.0 +offset_right = 550.0 +offset_bottom = 363.0 +text = "A free hanging rope (fixate_begin = false) controlled by two handles" + +[node name="RopeHandleBegin" type="Marker2D" parent="."] +position = Vector2(114, 428) +script = ExtResource("1_n2oah") +rope_path = NodePath("../Rope2") +rope_position = 0.207 +strength = 1.0 + +[node name="RopeHandleEnd" type="Marker2D" parent="."] +position = Vector2(203, 415) +script = ExtResource("1_n2oah") +rope_path = NodePath("../Rope2") +rope_position = 0.775 +strength = 1.0 + +[node name="Rope2" type="Node2D" parent="."] +position = Vector2(168, 403) +script = ExtResource("3_h0p3k") +num_segments = 20 +rope_length = 200.0 +fixate_begin = false +metadata/_edit_group_ = true + +[node name="Label5" type="Label" parent="."] +offset_left = 700.0 +offset_top = 241.0 +offset_right = 1219.0 +offset_bottom = 264.0 +text = "Smoothed handle transition enabled" + +[node name="AnimationPlayer" type="AnimationPlayer" parent="."] +libraries = { +"": SubResource("AnimationLibrary_agh7j") +} +script = ExtResource("3_g6662") + +[node name="RopeHandle3" type="Marker2D" parent="."] +position = Vector2(873, 301) +script = ExtResource("1_n2oah") +rope_path = NodePath("../Rope4") +rope_position = 0.531 +smoothing = true +position_smoothing_speed = 3.705 +strength = 1.0 + +[node name="Rope4" type="Node2D" parent="."] +position = Vector2(799, 297) +script = ExtResource("3_h0p3k") +num_segments = 20 +rope_length = 200.0 +metadata/_edit_group_ = true + +[node name="Label6" type="Label" parent="."] +offset_left = 304.0 +offset_top = 511.0 +offset_right = 823.0 +offset_bottom = 534.0 +text = "max_endpoint_distance limits the distance between both rope endpoints." + +[node name="Rope5" type="Node2D" parent="."] +position = Vector2(356, 591) +script = ExtResource("3_h0p3k") +max_endpoint_distance = 150.0 +fixate_begin = false + +[node name="RopeHandleBegin" type="Marker2D" parent="Rope5"] +position = Vector2(-10, -4) +script = ExtResource("1_n2oah") +rope_path = NodePath("..") +rope_position = 0.0 +strength = 1.0 + +[node name="RopeHandleEnd" type="Marker2D" parent="Rope5"] +position = Vector2(204, 22) +script = ExtResource("1_n2oah") +rope_path = NodePath("..") +strength = 1.0 + +[node name="Rope6" type="Node2D" parent="."] +position = Vector2(636, 564) +script = ExtResource("3_h0p3k") +max_endpoint_distance = 150.0 + +[node name="RopeHandleEnd" type="Marker2D" parent="Rope6"] +position = Vector2(160, 86) +script = ExtResource("1_n2oah") +rope_path = NodePath("..") +strength = 1.0 diff --git a/demo/icon.svg b/demo/rope_examples/icon.svg similarity index 100% rename from demo/icon.svg rename to demo/rope_examples/icon.svg diff --git a/demo/icon.svg.import b/demo/rope_examples/icon.svg.import similarity index 76% rename from demo/icon.svg.import rename to demo/rope_examples/icon.svg.import index b22e550..b760018 100644 --- a/demo/icon.svg.import +++ b/demo/rope_examples/icon.svg.import @@ -3,15 +3,15 @@ importer="texture" type="CompressedTexture2D" uid="uid://criwv6nuivcxy" -path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" +path="res://.godot/imported/icon.svg-4a363dd8a0910a06d0eee7b7bad8c85b.ctex" metadata={ "vram_texture": false } [deps] -source_file="res://icon.svg" -dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"] +source_file="res://rope_examples/icon.svg" +dest_files=["res://.godot/imported/icon.svg-4a363dd8a0910a06d0eee7b7bad8c85b.ctex"] [params] diff --git a/demo/rope_examples/line_renderer.tscn b/demo/rope_examples/line_renderer.tscn new file mode 100644 index 0000000..c72f628 --- /dev/null +++ b/demo/rope_examples/line_renderer.tscn @@ -0,0 +1,366 @@ +[gd_scene load_steps=10 format=3 uid="uid://g4jonp46a2qd"] + +[ext_resource type="Script" path="res://addons/ropesim/RopeHandle.gd" id="1_1x8kc"] +[ext_resource type="Script" path="res://rope_examples/scripts/animation_player.gd" id="1_ip83o"] +[ext_resource type="Texture2D" uid="uid://criwv6nuivcxy" path="res://rope_examples/icon.svg" id="3_dh00w"] +[ext_resource type="Script" path="res://addons/ropesim/Rope.gd" id="4_5xugj"] +[ext_resource type="Script" path="res://addons/ropesim/RopeRendererLine2D.gd" id="5_rievp"] + +[sub_resource type="Animation" id="Animation_m8wea"] +length = 0.001 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("Node2D2:position") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 0, +"values": [Vector2(107, 86)] +} +tracks/1/type = "value" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath("Rope4:rope_length") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 0, +"values": [200.0] +} + +[sub_resource type="Animation" id="Animation_sw2bi"] +resource_name = "moving" +length = 1.5 +loop_mode = 1 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("Node2D2:position") +tracks/0/interp = 2 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0, 0.733333), +"transitions": PackedFloat32Array(1, 1), +"update": 0, +"values": [Vector2(107, 86), Vector2(193, 86)] +} +tracks/1/type = "value" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath("Rope4:rope_length") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(0, 0.733333), +"transitions": PackedFloat32Array(1, 1), +"update": 0, +"values": [200.0, 100.0] +} + +[sub_resource type="AnimationLibrary" id="AnimationLibrary_ny3rx"] +_data = { +"RESET": SubResource("Animation_m8wea"), +"moving": SubResource("Animation_sw2bi") +} + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_xi23p"] +bg_color = Color(1, 1, 1, 1) +border_color = Color(1, 1, 1, 1) +expand_margin_left = 2.0 +expand_margin_right = 2.0 + +[node name="main" type="Node2D"] + +[node name="AnimationPlayer" type="AnimationPlayer" parent="."] +libraries = { +"": SubResource("AnimationLibrary_ny3rx") +} +script = ExtResource("1_ip83o") + +[node name="RopeRendererLine2D" type="Line2D" parent="."] +show_behind_parent = true +texture_repeat = 2 +position = Vector2(81, 457) +points = PackedVector2Array(-25, -8, -20.7475, 1.44524, -15.687, 10.4419, -9.18967, 18.3892, -0.636871, 23.9551, 9.50717, 24.7611, 18.6586, 20.3429, 25.7304, 13.1054, 31.1967, 4.66277, 35.1485, -4.52231, 45.1295, -4.63291, 44.6916, 5.83709, 44.575, 16.4508, 44.5429, 27.1293, 44.5313, 37.8096, 44.5235, 48.4408, 44.5157, 58.9835, 44.5077, 69.4093, 44.4992, 79.6994, 44.4905, 89.8445, 44.4813, 99.8445) +texture = ExtResource("3_dh00w") +texture_mode = 1 +script = ExtResource("5_rievp") +target_rope_path = NodePath("") + +[node name="paused_rope" type="Node2D" parent="."] +position = Vector2(314, 537) +script = ExtResource("4_5xugj") +pause = true +num_segments = 20 +rope_length = 200.0 +render_line = false +metadata/_edit_group_ = true + +[node name="RopeRendererLine2D" type="Line2D" parent="paused_rope"] +show_behind_parent = true +texture_repeat = 2 +position = Vector2(25, 8) +points = PackedVector2Array(-25, -8, -22.7762, 3.04263, -20.4138, 13.9832, -17.8192, 24.771, -14.9128, 35.3586, -11.5991, 45.6908, -7.74203, 55.6878, -3.13376, 65.2097, 2.54843, 73.9766, 9.77274, 81.366, 18.9411, 86.0076, 29.2663, 85.9136, 38.5399, 81.0415, 45.9161, 73.3553, 51.7538, 64.2748, 56.4952, 54.5067, 60.4478, 44.4342, 63.8161, 34.2926, 66.7439, 24.2285, 69.3381, 14.3219, 71.6804, 4.6001) +texture = ExtResource("3_dh00w") +texture_mode = 1 +script = ExtResource("5_rievp") + +[node name="RopeHandle" type="Marker2D" parent="paused_rope"] +position = Vector2(100, -4) +script = ExtResource("1_1x8kc") +rope_path = NodePath("..") + +[node name="Label" type="Label" parent="."] +offset_right = 40.0 +offset_bottom = 23.0 +text = "The RopeRenderLine2D node can be used to render the rope with a texture. +It is based on Line2D." + +[node name="Label2" type="Label" parent="."] +offset_left = 27.0 +offset_top = 359.0 +offset_right = 498.0 +offset_bottom = 434.0 +text = "The rope renderer node can be removed from the rope to effectively \"bake\" the rope into static geometry. +Useful when no live simulation is required." +autowrap_mode = 2 + +[node name="Label3" type="Label" parent="."] +offset_left = 169.0 +offset_top = 646.0 +offset_right = 640.0 +offset_bottom = 721.0 +text = "Alternatively, the rope can be paused to stop simulation. +The rope renderer will keep the last known position." +autowrap_mode = 2 + +[node name="Label4" type="Label" parent="."] +offset_left = 829.0 +offset_top = 9.0 +offset_right = 1415.0 +offset_bottom = 110.0 +text = "The position_mode determines where the rope renderer renders the rope. +Here it is rendered at the rope renderer's position." +autowrap_mode = 2 + +[node name="Label7" type="Label" parent="."] +offset_left = 1108.0 +offset_top = 178.0 +offset_right = 1694.0 +offset_bottom = 279.0 +text = "Multiple renderers can be assigned to the same rope." +autowrap_mode = 2 + +[node name="Label5" type="Label" parent="."] +offset_left = 764.0 +offset_top = 561.0 +offset_right = 1350.0 +offset_bottom = 662.0 +text = "The rope can also be rendered the rope node's position." +autowrap_mode = 2 + +[node name="Label6" type="Label" parent="."] +offset_left = 1086.0 +offset_top = 781.0 +offset_right = 1672.0 +offset_bottom = 882.0 +text = "When the rope has fixate_begin = false, it is usually best to use the \"use rope's first point\" position mode." +autowrap_mode = 2 + +[node name="Label8" type="Label" parent="."] +offset_left = 1734.0 +offset_top = 11.0 +offset_right = 2320.0 +offset_bottom = 112.0 +text = "The \"invert\" option reverses the point order, which also reverses the texture. +Mostly useful in cases where the rope should be extensible/retractable, as it looks more natural." +autowrap_mode = 2 + +[node name="Label9" type="Label" parent="."] +offset_left = 1789.0 +offset_top = 139.0 +offset_right = 1909.0 +offset_bottom = 162.0 +text = "invert = false" +autowrap_mode = 2 + +[node name="Label10" type="Label" parent="."] +offset_left = 1995.0 +offset_top = 139.0 +offset_right = 2115.0 +offset_bottom = 162.0 +text = "invert = true" +autowrap_mode = 2 + +[node name="Node2D" type="Node2D" parent="."] +position = Vector2(1057, 225) + +[node name="RopeRendererLine2D5" type="Line2D" parent="Node2D"] +show_behind_parent = true +texture_repeat = 2 +position = Vector2(-133, -132) +points = PackedVector2Array(0, 0, -0.722794, 12.932, -1.23862, 25.7996, -1.49449, 38.5739, -1.52303, 51.2288, -1.39105, 63.7453, -1.16194, 76.1113, -0.880508, 88.3193, -0.5728, 100.365, -0.251495, 112.244, 0.0788269, 123.955, 0.417938, 135.498, 0.767776, 146.87, 1.13104, 158.071, 1.51077, 169.1, 1.91002, 179.956, 2.33171, 190.639, 2.77904, 201.149, 3.25516, 211.484, 3.76396, 221.644, 4.30963, 231.629) +texture = ExtResource("3_dh00w") +texture_mode = 1 +script = ExtResource("5_rievp") +target_rope_path = NodePath("../../Node2D2/Rope") +position_mode = 0 + +[node name="RopeRendererLine2D" type="Line2D" parent="Node2D"] +show_behind_parent = true +texture_repeat = 2 +position = Vector2(307, 1) +points = PackedVector2Array(0, 0, -0.722794, 12.932, -1.23862, 25.7996, -1.49449, 38.5739, -1.52303, 51.2288, -1.39105, 63.7453, -1.16194, 76.1113, -0.880508, 88.3193, -0.5728, 100.365, -0.251495, 112.244, 0.0788269, 123.955, 0.417938, 135.498, 0.767776, 146.87, 1.13104, 158.071, 1.51077, 169.1, 1.91002, 179.956, 2.33171, 190.639, 2.77904, 201.149, 3.25516, 211.484, 3.76396, 221.644, 4.30963, 231.629) +texture = ExtResource("3_dh00w") +texture_mode = 1 +script = ExtResource("5_rievp") +target_rope_path = NodePath("../../Node2D2/Rope") +position_mode = 0 + +[node name="RopeRendererLine2D2" type="Line2D" parent="Node2D"] +show_behind_parent = true +texture_repeat = 2 +position = Vector2(252, 5) +points = PackedVector2Array(0, 0, -0.722794, 12.932, -1.23862, 25.7996, -1.49449, 38.5739, -1.52303, 51.2288, -1.39105, 63.7453, -1.16194, 76.1113, -0.880508, 88.3193, -0.5728, 100.365, -0.251495, 112.244, 0.0788269, 123.955, 0.417938, 135.498, 0.767776, 146.87, 1.13104, 158.071, 1.51077, 169.1, 1.91002, 179.956, 2.33171, 190.639, 2.77904, 201.149, 3.25516, 211.484, 3.76396, 221.644, 4.30963, 231.629) +texture = ExtResource("3_dh00w") +texture_mode = 1 +script = ExtResource("5_rievp") +target_rope_path = NodePath("../../Node2D2/Rope") +position_mode = 0 + +[node name="RopeRendererLine2D3" type="Line2D" parent="Node2D"] +show_behind_parent = true +texture_repeat = 2 +position = Vector2(201, 12) +points = PackedVector2Array(0, 0, -0.722794, 12.932, -1.23862, 25.7996, -1.49449, 38.5739, -1.52303, 51.2288, -1.39105, 63.7453, -1.16194, 76.1113, -0.880508, 88.3193, -0.5728, 100.365, -0.251495, 112.244, 0.0788269, 123.955, 0.417938, 135.498, 0.767776, 146.87, 1.13104, 158.071, 1.51077, 169.1, 1.91002, 179.956, 2.33171, 190.639, 2.77904, 201.149, 3.25516, 211.484, 3.76396, 221.644, 4.30963, 231.629) +texture = ExtResource("3_dh00w") +texture_mode = 1 +script = ExtResource("5_rievp") +target_rope_path = NodePath("../../Node2D2/Rope") +position_mode = 0 + +[node name="RopeRendererLine2D4" type="Line2D" parent="Node2D"] +show_behind_parent = true +texture_repeat = 2 +position = Vector2(140, 19) +points = PackedVector2Array(0, 0, -0.722794, 12.932, -1.23862, 25.7996, -1.49449, 38.5739, -1.52303, 51.2288, -1.39105, 63.7453, -1.16194, 76.1113, -0.880508, 88.3193, -0.5728, 100.365, -0.251495, 112.244, 0.0788269, 123.955, 0.417938, 135.498, 0.767776, 146.87, 1.13104, 158.071, 1.51077, 169.1, 1.91002, 179.956, 2.33171, 190.639, 2.77904, 201.149, 3.25516, 211.484, 3.76396, 221.644, 4.30963, 231.629) +texture = ExtResource("3_dh00w") +texture_mode = 1 +script = ExtResource("5_rievp") +target_rope_path = NodePath("../../Node2D2/Rope") +position_mode = 0 + +[node name="Node2D2" type="Node2D" parent="."] +position = Vector2(107, 86) + +[node name="Rope" type="Node2D" parent="Node2D2"] +script = ExtResource("4_5xugj") +num_segments = 20 +rope_length = 200.0 +render_line = false +metadata/_edit_group_ = true + +[node name="RopeRendererLine2D" type="Line2D" parent="Node2D2/Rope"] +show_behind_parent = true +texture_repeat = 2 +position = Vector2(25, 8) +points = PackedVector2Array(-18.1114, -8, -18.8342, 4.93199, -19.35, 17.7996, -19.6059, 30.5739, -19.6344, 43.2288, -19.5024, 55.7453, -19.2733, 68.1113, -18.9919, 80.3193, -18.6842, 92.3645, -18.3629, 104.244, -18.0325, 115.955, -17.6934, 127.498, -17.3436, 138.87, -16.9803, 150.071, -16.6006, 161.1, -16.2014, 171.956, -15.7797, 182.639, -15.3323, 193.149, -14.8562, 203.484, -14.3474, 213.644, -13.8017, 223.629) +texture = ExtResource("3_dh00w") +texture_mode = 1 +script = ExtResource("5_rievp") + +[node name="Rope2" type="Node2D" parent="."] +position = Vector2(886.041, 601) +script = ExtResource("4_5xugj") +num_segments = 20 +rope_length = 200.0 +render_line = false +metadata/_edit_group_ = true + +[node name="RopeRendererLine2D2" type="Line2D" parent="Rope2"] +show_behind_parent = true +texture_repeat = 2 +position = Vector2(228, 71) +points = PackedVector2Array(-228, -71, -228, -58.1105, -228, -45.2946, -228, -32.5795, -228, -19.9864, -228, -7.53052, -228, 4.77734, -228, 16.93, -228, 28.9226, -228, 40.7524, -228, 52.4175, -228, 63.917, -228, 75.2502, -228, 86.4171, -228, 97.4172, -228, 108.251, -228, 118.917, -228, 129.418, -228, 139.751, -228, 149.918, -228, 159.918) +texture = ExtResource("3_dh00w") +texture_mode = 1 +script = ExtResource("5_rievp") +position_mode = 1 + +[node name="Rope3" type="Node2D" parent="."] +position = Vector2(1167.04, 658) +script = ExtResource("4_5xugj") +num_segments = 20 +rope_length = 200.0 +fixate_begin = false +render_line = false +metadata/_edit_group_ = true + +[node name="RopeRendererLine2D3" type="Line2D" parent="Rope3"] +show_behind_parent = true +texture_repeat = 2 +position = Vector2(288.021, 8) +points = PackedVector2Array(-288.021, -8, -285.644, 2.92297, -283.11, 13.7374, -280.312, 24.3867, -277.158, 34.8157, -273.534, 44.956, -269.274, 54.7013, -264.121, 63.8546, -257.677, 71.9983, -249.436, 78.1768, -239.419, 80.5198, -229.445, 77.6694, -221.27, 71.0529, -214.873, 62.5068, -209.75, 52.9601, -205.514, 42.8307, -201.914, 32.3325, -198.789, 21.5969, -196.029, 10.7188, -193.536, -0.223572, -191.433, -10) +texture = ExtResource("3_dh00w") +texture_mode = 1 +script = ExtResource("5_rievp") + +[node name="RopeHandle" type="Marker2D" parent="Rope3"] +script = ExtResource("1_1x8kc") +rope_path = NodePath("..") +rope_position = 0.0 +strength = 1.0 + +[node name="RopeHandle2" type="Marker2D" parent="Rope3"] +position = Vector2(96.5881, -2) +script = ExtResource("1_1x8kc") +rope_path = NodePath("..") +strength = 1.0 + +[node name="Rope4" type="Node2D" parent="."] +position = Vector2(1845, 189) +script = ExtResource("4_5xugj") +num_segments = 20 +rope_length = 200.0 +render_line = false +metadata/_edit_group_ = true + +[node name="RopeRendererLine2D" type="Line2D" parent="Rope4"] +show_behind_parent = true +texture_repeat = 2 +position = Vector2(25, 8) +points = PackedVector2Array(-25, -8, -25, 0.0043335, -25, 7.98198, -25, 15.9263, -25, 23.8346, -25, 31.7076, -25, 39.5482, -25, 47.3604, -25, 55.1487, -25, 62.9175, -25, 70.6711, -25, 78.4129, -25, 86.1458, -25, 93.8721, -25, 101.594, -25, 109.312, -25, 117.028, -25, 124.742, -25, 132.454, -25, 140.166, -25, 147.877) +texture = ExtResource("3_dh00w") +texture_mode = 1 +script = ExtResource("5_rievp") +target_rope_path = NodePath("../../Rope4") + +[node name="RopeRendererLine2D_invert" type="Line2D" parent="Rope4"] +show_behind_parent = true +texture_repeat = 2 +position = Vector2(203, 0) +points = PackedVector2Array(0, 155.877, 0, 148.166, 0, 140.454, 0, 132.742, 0, 125.028, 0, 117.312, 0, 109.594, 0, 101.872, 0, 94.1458, 0, 86.4129, 0, 78.6711, 0, 70.9175, 0, 63.1487, 0, 55.3604, 0, 47.5482, 0, 39.7076, 0, 31.8346, 0, 23.9263, 0, 15.982, 0, 8.00433, 0, 0) +texture = ExtResource("3_dh00w") +texture_mode = 1 +script = ExtResource("5_rievp") +position_mode = 0 +invert = true + +[node name="VSeparator2" type="VSeparator" parent="."] +offset_left = 582.0 +offset_top = -60.0 +offset_right = 786.0 +offset_bottom = 857.0 +theme_override_styles/separator = SubResource("StyleBoxFlat_xi23p") + +[node name="VSeparator3" type="VSeparator" parent="."] +offset_left = 1591.0 +offset_top = -25.0 +offset_right = 1795.0 +offset_bottom = 892.0 +theme_override_styles/separator = SubResource("StyleBoxFlat_xi23p") diff --git a/demo/rope_examples/rope_properties.tscn b/demo/rope_examples/rope_properties.tscn new file mode 100644 index 0000000..679718d --- /dev/null +++ b/demo/rope_examples/rope_properties.tscn @@ -0,0 +1,534 @@ +[gd_scene load_steps=10 format=3 uid="uid://clyutgr2fativ"] + +[ext_resource type="Script" path="res://rope_examples/scripts/animation_player.gd" id="1_mv383"] +[ext_resource type="Script" path="res://addons/ropesim/RopeHandle.gd" id="3_mb3ny"] +[ext_resource type="Script" path="res://addons/ropesim/Rope.gd" id="4_rnvio"] + +[sub_resource type="Animation" id="Animation_t4gml"] +length = 0.001 + +[sub_resource type="Animation" id="Animation_c3bfa"] +resource_name = "moving" +loop_mode = 1 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("Node2D2:position") +tracks/0/interp = 2 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0, 0.3), +"transitions": PackedFloat32Array(1, 1), +"update": 0, +"values": [Vector2(199, 113), Vector2(140, 150)] +} + +[sub_resource type="AnimationLibrary" id="AnimationLibrary_13y75"] +_data = { +"RESET": SubResource("Animation_t4gml"), +"moving": SubResource("Animation_c3bfa") +} + +[sub_resource type="Curve" id="Curve_2i72k"] +_data = [Vector2(0.134615, 1), 0.0, 0.0, 0, 0, Vector2(0.485577, 0.484536), 0.0, -0.0200405, 0, 1, Vector2(1, 0.474227), 0.231403, 0.0, 0, 0] +point_count = 3 + +[sub_resource type="StyleBoxLine" id="StyleBoxLine_uwde6"] +color = Color(1, 1, 1, 1) +thickness = 4 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_f4u3w"] +bg_color = Color(1, 1, 1, 1) +border_color = Color(1, 1, 1, 1) +expand_margin_left = 2.0 +expand_margin_right = 2.0 + +[node name="main" type="Node2D"] + +[node name="AnimationPlayer" type="AnimationPlayer" parent="."] +libraries = { +"": SubResource("AnimationLibrary_13y75") +} +script = ExtResource("1_mv383") + +[node name="Rope15" type="Node2D" parent="."] +position = Vector2(140.181, 1438) +script = ExtResource("4_rnvio") +rope_length = 200.0 +num_constraint_iterations = 1 +fixate_begin = false +render_debug = true +metadata/_edit_group_ = true + +[node name="RopeHandle" type="Marker2D" parent="Rope15"] +position = Vector2(-106.181, 0) +script = ExtResource("3_mb3ny") +rope_path = NodePath("..") +rope_position = 0.0 +strength = 1.0 + +[node name="RopeHandle2" type="Marker2D" parent="Rope15"] +position = Vector2(93.8187, 0) +script = ExtResource("3_mb3ny") +rope_path = NodePath("..") +strength = 1.0 + +[node name="Rope16" type="Node2D" parent="."] +position = Vector2(365, 1438) +script = ExtResource("4_rnvio") +rope_length = 200.0 +num_constraint_iterations = 5 +fixate_begin = false +render_debug = true +metadata/_edit_group_ = true + +[node name="RopeHandle" type="Marker2D" parent="Rope16"] +position = Vector2(-106.181, 0) +script = ExtResource("3_mb3ny") +rope_path = NodePath("..") +rope_position = 0.0 +strength = 1.0 + +[node name="RopeHandle2" type="Marker2D" parent="Rope16"] +position = Vector2(93.8187, 0) +script = ExtResource("3_mb3ny") +rope_path = NodePath("..") +strength = 1.0 + +[node name="Rope17" type="Node2D" parent="."] +position = Vector2(583, 1438) +script = ExtResource("4_rnvio") +rope_length = 200.0 +fixate_begin = false +render_debug = true +metadata/_edit_group_ = true + +[node name="RopeHandle" type="Marker2D" parent="Rope17"] +position = Vector2(-106.181, 0) +script = ExtResource("3_mb3ny") +rope_path = NodePath("..") +rope_position = 0.0 +strength = 1.0 + +[node name="RopeHandle2" type="Marker2D" parent="Rope17"] +position = Vector2(93.819, 0) +script = ExtResource("3_mb3ny") +rope_path = NodePath("..") +strength = 1.0 + +[node name="Rope18" type="Node2D" parent="."] +position = Vector2(807, 1438) +script = ExtResource("4_rnvio") +rope_length = 200.0 +num_constraint_iterations = 50 +fixate_begin = false +render_debug = true +metadata/_edit_group_ = true + +[node name="RopeHandle" type="Marker2D" parent="Rope18"] +position = Vector2(-106.181, 0) +script = ExtResource("3_mb3ny") +rope_path = NodePath("..") +rope_position = 0.0 +strength = 1.0 + +[node name="RopeHandle2" type="Marker2D" parent="Rope18"] +position = Vector2(93.819, 0) +script = ExtResource("3_mb3ny") +rope_path = NodePath("..") +strength = 1.0 + +[node name="Node2D2" type="Node2D" parent="."] +position = Vector2(180.7, 124.476) + +[node name="Rope3" type="Node2D" parent="Node2D2"] +position = Vector2(-88.3748, -22.2188) +script = ExtResource("4_rnvio") +num_segments = 5 +rope_length = 200.0 +gravity = 39.41 +render_debug = true +metadata/_edit_group_ = true + +[node name="Rope" type="Node2D" parent="Node2D2"] +position = Vector2(114.151, -22.2188) +script = ExtResource("4_rnvio") +rope_length = 200.0 +gravity = 39.41 +render_debug = true +metadata/_edit_group_ = true + +[node name="Rope2" type="Node2D" parent="Node2D2"] +position = Vector2(308.625, -22.2188) +script = ExtResource("4_rnvio") +num_segments = 20 +rope_length = 200.0 +gravity = 39.41 +render_debug = true +metadata/_edit_group_ = true + +[node name="Rope12" type="Node2D" parent="Node2D2"] +position = Vector2(687.625, -15.2188) +script = ExtResource("4_rnvio") +num_segments = 5 +rope_length = 200.0 +gravity = 39.41 +render_debug = true +metadata/_edit_group_ = true + +[node name="Rope13" type="Node2D" parent="Node2D2"] +position = Vector2(890.151, -15.2188) +script = ExtResource("4_rnvio") +num_segments = 5 +rope_length = 200.0 +segment_length_distribution = SubResource("Curve_2i72k") +gravity = 39.41 +render_debug = true +metadata/_edit_group_ = true + +[node name="Rope4" type="Node2D" parent="Node2D2"] +position = Vector2(-58.8187, 906.801) +script = ExtResource("4_rnvio") +rope_length = 200.0 +num_constraint_iterations = 1 +render_debug = true +metadata/_edit_group_ = true + +[node name="Rope5" type="Node2D" parent="Node2D2"] +position = Vector2(166.181, 906.801) +script = ExtResource("4_rnvio") +rope_length = 200.0 +num_constraint_iterations = 5 +render_debug = true +metadata/_edit_group_ = true + +[node name="Rope6" type="Node2D" parent="Node2D2"] +position = Vector2(377.181, 906.801) +script = ExtResource("4_rnvio") +rope_length = 200.0 +render_debug = true +metadata/_edit_group_ = true + +[node name="Rope7" type="Node2D" parent="Node2D2"] +position = Vector2(603, 906.801) +script = ExtResource("4_rnvio") +rope_length = 200.0 +num_constraint_iterations = 50 +render_debug = true +metadata/_edit_group_ = true + +[node name="Rope8" type="Node2D" parent="Node2D2"] +position = Vector2(-54.1407, 436.104) +script = ExtResource("4_rnvio") +rope_length = 200.0 +gravity = 20.0 +render_debug = true +metadata/_edit_group_ = true + +[node name="Rope9" type="Node2D" parent="Node2D2"] +position = Vector2(148.859, 436.104) +script = ExtResource("4_rnvio") +rope_length = 200.0 +num_constraint_iterations = 5 +render_debug = true +metadata/_edit_group_ = true + +[node name="Rope14" type="Node2D" parent="Node2D2"] +position = Vector2(377.859, 435.104) +script = ExtResource("4_rnvio") +rope_length = 200.0 +gravity_direction = Vector2(0, -1) +num_constraint_iterations = 5 +render_debug = true +metadata/_edit_group_ = true + +[node name="Rope10" type="Node2D" parent="Node2D2"] +position = Vector2(846.707, 474.556) +script = ExtResource("4_rnvio") +rope_length = 200.0 +gravity = 40.0 +render_debug = true +metadata/_edit_group_ = true + +[node name="Rope11" type="Node2D" parent="Node2D2"] +position = Vector2(1049.71, 474.556) +script = ExtResource("4_rnvio") +rope_length = 200.0 +gravity = 40.0 +damping = 3.0 +num_constraint_iterations = 5 +render_debug = true +metadata/_edit_group_ = true + +[node name="Label3" type="Label" parent="."] +offset_left = 35.0 +offset_top = 40.0 +offset_right = 134.0 +offset_bottom = 63.0 +text = "5 Segments" +metadata/_edit_use_anchors_ = true + +[node name="Label" type="Label" parent="."] +offset_left = 238.0 +offset_top = 40.0 +offset_right = 337.0 +offset_bottom = 63.0 +text = "10 Segments" +metadata/_edit_use_anchors_ = true + +[node name="Label2" type="Label" parent="."] +offset_left = 432.0 +offset_top = 40.0 +offset_right = 531.0 +offset_bottom = 63.0 +text = "20 Segments" +metadata/_edit_use_anchors_ = true + +[node name="Label14" type="Label" parent="."] +offset_left = 811.0 +offset_top = 47.0 +offset_right = 910.0 +offset_bottom = 70.0 +text = "Default" +metadata/_edit_use_anchors_ = true + +[node name="Label17" type="Label" parent="."] +offset_left = 788.0 +offset_top = -67.0 +offset_right = 1602.0 +offset_bottom = 8.0 +text = "Segment length distribution curve allows changing the length of rope segments. +Useful to increase quality in specific areas, while keeping the same segment count. +However, it can alter the physical behavior." +metadata/_edit_use_anchors_ = true + +[node name="Label15" type="Label" parent="."] +offset_left = 1014.0 +offset_top = 47.0 +offset_right = 1113.0 +offset_bottom = 70.0 +text = "With segment length distribution curve. +More segments towards the end, less at the beginning." +metadata/_edit_use_anchors_ = true + +[node name="Label4" type="Label" parent="."] +offset_left = 32.0 +offset_top = 974.0 +offset_right = 215.0 +offset_bottom = 997.0 +text = "1 Constraint iteration" + +[node name="Label13" type="Label" parent="."] +offset_left = 31.0 +offset_top = 878.0 +offset_right = 976.0 +offset_bottom = 953.0 +text = "The more constraint iterations the less the rope stretches. +Depending on the forces applied to the rope, more or less constraint iterations may be necessary to properly constrain it.q +Less constraint iterations can be used to make a rope more bouncy." + +[node name="Label18" type="Label" parent="."] +offset_left = 31.0 +offset_top = 1327.0 +offset_right = 1020.0 +offset_bottom = 1402.0 +text = "The ropes below all have the same gravity and are fixated on both ends. +Lower constraint iteration have difficulties to properly contract the rope. +Unlike above, the effect is much more visible, which shows that the amount of constraint iterations needed is highly dependent on the use case." + +[node name="Label5" type="Label" parent="."] +offset_left = 361.0 +offset_top = 974.0 +offset_right = 460.0 +offset_bottom = 997.0 +text = "5 +" + +[node name="Label6" type="Label" parent="."] +offset_left = 565.0 +offset_top = 975.0 +offset_right = 664.0 +offset_bottom = 998.0 +text = "10 +" + +[node name="Label7" type="Label" parent="."] +offset_left = 795.0 +offset_top = 975.0 +offset_right = 894.0 +offset_bottom = 998.0 +text = "50" + +[node name="Label8" type="Label" parent="."] +offset_left = 26.0 +offset_top = 510.0 +offset_right = 209.0 +offset_bottom = 533.0 +text = "Gravity 20" + +[node name="Label9" type="Label" parent="."] +offset_left = 276.0 +offset_top = 510.0 +offset_right = 375.0 +offset_bottom = 533.0 +text = "Gravity 100 +" + +[node name="Label16" type="Label" parent="."] +offset_left = 499.0 +offset_top = 610.0 +offset_right = 658.0 +offset_bottom = 633.0 +text = "Gravity 100 upwards +" + +[node name="Label10" type="Label" parent="."] +offset_left = 949.0 +offset_top = 517.0 +offset_right = 1132.0 +offset_bottom = 540.0 +text = "Damping 0.0" + +[node name="Label12" type="Label" parent="."] +offset_left = 1028.0 +offset_top = 477.0 +offset_right = 1252.0 +offset_bottom = 500.0 +text = "Damping is similar to friction" + +[node name="Label11" type="Label" parent="."] +offset_left = 1199.0 +offset_top = 517.0 +offset_right = 1298.0 +offset_bottom = 540.0 +text = "Damping 3.0 +" + +[node name="Label19" type="Label" parent="."] +offset_left = 1267.0 +offset_top = 905.0 +offset_right = 1366.0 +offset_bottom = 928.0 +text = "max_endpoint_distance provides an easy way to constraint the rope length when using handles. +" + +[node name="Label22" type="Label" parent="."] +offset_left = 1267.0 +offset_top = 1328.0 +offset_right = 2007.0 +offset_bottom = 1351.0 +text = "max_endpoint_distance only considers the distance between both endpoints. +Does not consider the actual rope length. +" + +[node name="Label20" type="Label" parent="."] +offset_left = 1236.0 +offset_top = 956.0 +offset_right = 1460.0 +offset_bottom = 979.0 +text = "max_endpoint_distance = -1" + +[node name="Label21" type="Label" parent="."] +offset_left = 1609.0 +offset_top = 956.0 +offset_right = 1837.0 +offset_bottom = 979.0 +text = "max_endpoint_distance = 100" + +[node name="HSeparator" type="HSeparator" parent="."] +offset_left = 24.0 +offset_top = 417.0 +offset_right = 1697.0 +offset_bottom = 421.0 +theme_override_styles/separator = SubResource("StyleBoxLine_uwde6") + +[node name="HSeparator2" type="HSeparator" parent="."] +offset_left = 2.0 +offset_top = 848.0 +offset_right = 1675.0 +offset_bottom = 852.0 +theme_override_styles/separator = SubResource("StyleBoxLine_uwde6") + +[node name="VSeparator" type="VSeparator" parent="."] +offset_left = 648.0 +offset_top = -77.0 +offset_right = 852.0 +offset_bottom = 848.0 +theme_override_styles/separator = SubResource("StyleBoxFlat_f4u3w") + +[node name="VSeparator2" type="VSeparator" parent="."] +offset_left = 1105.0 +offset_top = 854.0 +offset_right = 1309.0 +offset_bottom = 1771.0 +theme_override_styles/separator = SubResource("StyleBoxFlat_f4u3w") + +[node name="Rope" type="Node2D" parent="."] +position = Vector2(1317, 996) +script = ExtResource("4_rnvio") + +[node name="RopeHandle" type="Marker2D" parent="Rope"] +position = Vector2(136, 257) +script = ExtResource("3_mb3ny") +rope_path = NodePath("..") +strength = 1.0 + +[node name="Rope2" type="Node2D" parent="."] +position = Vector2(1697, 992) +script = ExtResource("4_rnvio") +max_endpoint_distance = 100.0 + +[node name="RopeHandle" type="Marker2D" parent="Rope2"] +position = Vector2(136, 257) +script = ExtResource("3_mb3ny") +rope_path = NodePath("..") +strength = 1.0 + +[node name="Label23" type="Label" parent="."] +offset_left = 1236.0 +offset_top = 1406.0 +offset_right = 1460.0 +offset_bottom = 1429.0 +text = "max_endpoint_distance = -1" + +[node name="Label24" type="Label" parent="."] +offset_left = 1609.0 +offset_top = 1406.0 +offset_right = 1837.0 +offset_bottom = 1429.0 +text = "max_endpoint_distance = 100" + +[node name="Rope4" type="Node2D" parent="."] +position = Vector2(1697, 1442) +script = ExtResource("4_rnvio") +max_endpoint_distance = 100.0 + +[node name="RopeHandle" type="Marker2D" parent="Rope4"] +position = Vector2(250, 56) +script = ExtResource("3_mb3ny") +rope_path = NodePath("..") +rope_position = 0.622 +strength = 1.0 + +[node name="RopeHandle2" type="Marker2D" parent="Rope4"] +position = Vector2(139, 249) +script = ExtResource("3_mb3ny") +rope_path = NodePath("..") +strength = 1.0 + +[node name="Rope5" type="Node2D" parent="."] +position = Vector2(1318, 1446) +script = ExtResource("4_rnvio") + +[node name="RopeHandle" type="Marker2D" parent="Rope5"] +position = Vector2(250, 56) +script = ExtResource("3_mb3ny") +rope_path = NodePath("..") +rope_position = 0.622 +strength = 1.0 + +[node name="RopeHandle2" type="Marker2D" parent="Rope5"] +position = Vector2(139, 249) +script = ExtResource("3_mb3ny") +rope_path = NodePath("..") +strength = 1.0 diff --git a/demo/rope_examples/rope_pulling.tscn b/demo/rope_examples/rope_pulling.tscn new file mode 100644 index 0000000..15df502 --- /dev/null +++ b/demo/rope_examples/rope_pulling.tscn @@ -0,0 +1,106 @@ +[gd_scene load_steps=7 format=3 uid="uid://ba2ll2csfyo28"] + +[ext_resource type="Script" path="res://addons/ropesim/Rope.gd" id="1_ayj28"] +[ext_resource type="Script" path="res://rope_examples/scripts/character_body_2d.gd" id="2_nvbmc"] +[ext_resource type="Texture2D" uid="uid://criwv6nuivcxy" path="res://rope_examples/icon.svg" id="3_8uo3w"] +[ext_resource type="Script" path="res://addons/ropesim/RopeInteraction.gd" id="6_qu4ri"] + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_fdqy7"] +size = Vector2(64, 64) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_7w0sw"] +size = Vector2(128, 128) + +[node name="Node2D" type="Node2D"] + +[node name="PlayerA" type="CharacterBody2D" parent="."] +process_physics_priority = -100 +position = Vector2(332, 155) +collision_layer = 0 +motion_mode = 1 +script = ExtResource("2_nvbmc") +metadata/_edit_group_ = true + +[node name="CollisionShape2D" type="CollisionShape2D" parent="PlayerA"] +shape = SubResource("RectangleShape2D_fdqy7") + +[node name="Icon" type="Sprite2D" parent="PlayerA"] +scale = Vector2(0.5, 0.5) +texture = ExtResource("3_8uo3w") + +[node name="Label2" type="Label" parent="PlayerA"] +offset_left = -46.0 +offset_top = 36.0 +offset_right = 42.0 +offset_bottom = 59.0 +text = "WSAD" +horizontal_alignment = 1 + +[node name="PlayerB" type="CharacterBody2D" parent="."] +process_physics_priority = -100 +position = Vector2(128, 153) +collision_layer = 0 +motion_mode = 1 +script = ExtResource("2_nvbmc") +use_arrow_keys = true +metadata/_edit_group_ = true + +[node name="CollisionShape2D" type="CollisionShape2D" parent="PlayerB"] +shape = SubResource("RectangleShape2D_fdqy7") + +[node name="Icon2" type="Sprite2D" parent="PlayerB"] +scale = Vector2(0.5, 0.5) +texture = ExtResource("3_8uo3w") + +[node name="Label" type="Label" parent="PlayerB"] +offset_left = -41.0 +offset_top = 35.0 +offset_right = 47.0 +offset_bottom = 58.0 +text = "Arrow Keys" +horizontal_alignment = 1 + +[node name="Rope" type="Node2D" parent="."] +position = Vector2(38, 156) +script = ExtResource("1_ayj28") +rope_length = 200.0 +max_endpoint_distance = 270.0 +num_constraint_iterations = 20 +fixate_begin = false +line_width = 6.0 +color = Color(0.533333, 0.384314, 0.258824, 1) + +[node name="RopeInteractionBegin" type="Node" parent="Rope" node_paths=PackedStringArray("target_node", "rope")] +script = ExtResource("6_qu4ri") +position_update_mode = 1 +target_node = NodePath("../../PlayerA") +rope = NodePath("..") +rope_position = 0.0 +strength = 1.0 + +[node name="RopeInteractionEnd" type="Node" parent="Rope" node_paths=PackedStringArray("target_node", "rope")] +script = ExtResource("6_qu4ri") +position_update_mode = 1 +target_node = NodePath("../../PlayerB") +rope = NodePath("..") +strength = 1.0 + +[node name="wall" type="StaticBody2D" parent="."] +z_index = -1 +position = Vector2(339, 330) +scale = Vector2(4.6, 0.519999) +metadata/_edit_group_ = true + +[node name="CollisionShape2D" type="CollisionShape2D" parent="wall"] +shape = SubResource("RectangleShape2D_7w0sw") + +[node name="Icon" type="Sprite2D" parent="wall"] +texture = ExtResource("3_8uo3w") + +[node name="Label2" type="Label" parent="."] +offset_left = 295.0 +offset_top = 366.0 +offset_right = 383.0 +offset_bottom = 389.0 +text = "Wall" +horizontal_alignment = 1 diff --git a/demo/rope_examples/rope_swinging.tscn b/demo/rope_examples/rope_swinging.tscn new file mode 100644 index 0000000..780202c --- /dev/null +++ b/demo/rope_examples/rope_swinging.tscn @@ -0,0 +1,127 @@ +[gd_scene load_steps=8 format=3 uid="uid://bdm3ftf6jghmq"] + +[ext_resource type="Script" path="res://rope_examples/scripts/character_body_2d_platformer.gd" id="1_kf7l7"] +[ext_resource type="Texture2D" uid="uid://criwv6nuivcxy" path="res://rope_examples/icon.svg" id="2_3o1qr"] +[ext_resource type="Script" path="res://addons/ropesim/Rope.gd" id="3_3cx2h"] +[ext_resource type="Script" path="res://addons/ropesim/RopeInteraction.gd" id="4_q3ugd"] +[ext_resource type="Script" path="res://addons/ropesim/RopeCollisionShapeGenerator.gd" id="5_wx3wk"] + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_fdqy7"] +size = Vector2(64, 64) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_7w0sw"] +size = Vector2(128, 128) + +[node name="Node2D" type="Node2D"] + +[node name="Label" type="Label" parent="."] +offset_left = 683.0 +offset_top = 14.0 +offset_right = 1132.0 +offset_bottom = 89.0 +text = "Simple example for swinging on a rope in a 2D platformer. +Not perfect but demonstrates a basic setup." + +[node name="Label2" type="Label" parent="."] +offset_left = 17.0 +offset_top = 14.0 +offset_right = 126.0 +offset_bottom = 89.0 +text = "A/D: Walk +Space: Jump +W/S: Climb" + +[node name="PlayerA" type="CharacterBody2D" parent="."] +process_physics_priority = -100 +position = Vector2(128, 210) +collision_layer = 0 +script = ExtResource("1_kf7l7") +metadata/_edit_group_ = true + +[node name="CollisionShape2D" type="CollisionShape2D" parent="PlayerA"] +shape = SubResource("RectangleShape2D_fdqy7") + +[node name="Icon" type="Sprite2D" parent="PlayerA"] +scale = Vector2(0.5, 0.5) +texture = ExtResource("2_3o1qr") + +[node name="Area2D" type="Area2D" parent="PlayerA"] +collision_layer = 0 +collision_mask = 2 +monitorable = false + +[node name="CollisionShape2D" type="CollisionShape2D" parent="PlayerA/Area2D"] +shape = SubResource("RectangleShape2D_fdqy7") + +[node name="RopeInteraction" type="Node" parent="PlayerA" node_paths=PackedStringArray("target_node")] +script = ExtResource("4_q3ugd") +enable = false +position_update_mode = 1 +target_node = NodePath("..") + +[node name="Rope" type="Node2D" parent="."] +position = Vector2(464, 4) +script = ExtResource("3_3cx2h") +num_segments = 15 +rope_length = 250.0 +max_endpoint_distance = 270.0 +num_constraint_iterations = 30 +line_width = 6.0 +color = Color(0.535156, 0.382734, 0.258426, 1) + +[node name="Area2D" type="Area2D" parent="Rope"] +collision_layer = 2 +collision_mask = 0 +monitoring = false + +[node name="RopeCollisionShapeGenerator" type="Node" parent="Rope/Area2D"] +script = ExtResource("5_wx3wk") +rope_path = NodePath("../..") + +[node name="wall" type="StaticBody2D" parent="."] +z_index = -1 +position = Vector2(146, 330) +scale = Vector2(2.2, 0.519999) +metadata/_edit_group_ = true + +[node name="CollisionShape2D" type="CollisionShape2D" parent="wall"] +shape = SubResource("RectangleShape2D_7w0sw") + +[node name="Icon" type="Sprite2D" parent="wall"] +texture = ExtResource("2_3o1qr") + +[node name="wall4" type="StaticBody2D" parent="."] +z_index = -1 +position = Vector2(219, 422) +scale = Vector2(2.2, 0.519999) +metadata/_edit_group_ = true + +[node name="CollisionShape2D" type="CollisionShape2D" parent="wall4"] +shape = SubResource("RectangleShape2D_7w0sw") + +[node name="Icon" type="Sprite2D" parent="wall4"] +texture = ExtResource("2_3o1qr") + +[node name="wall3" type="StaticBody2D" parent="."] +z_index = -1 +position = Vector2(538, 521) +scale = Vector2(10.88, 0.519999) +metadata/_edit_group_ = true + +[node name="CollisionShape2D" type="CollisionShape2D" parent="wall3"] +shape = SubResource("RectangleShape2D_7w0sw") + +[node name="Icon" type="Sprite2D" parent="wall3"] +texture = ExtResource("2_3o1qr") + +[node name="wall2" type="StaticBody2D" parent="."] +z_index = -1 +position = Vector2(829, 330) +scale = Vector2(2.2, 0.519999) +metadata/_edit_group_ = true + +[node name="CollisionShape2D" type="CollisionShape2D" parent="wall2"] +shape = SubResource("RectangleShape2D_7w0sw") + +[node name="Icon" type="Sprite2D" parent="wall2"] +texture = ExtResource("2_3o1qr") diff --git a/demo/rope_examples/scripts/PerformanceLabel.gd b/demo/rope_examples/scripts/PerformanceLabel.gd new file mode 100644 index 0000000..55c63fb --- /dev/null +++ b/demo/rope_examples/scripts/PerformanceLabel.gd @@ -0,0 +1,10 @@ +@tool +extends Label + + +func _enter_tree() -> void: + NativeRopeServer.on_post_update.connect(_on_post_update) + + +func _on_post_update() -> void: + text = "%s Ropes\n%.2f ms" % [ NativeRopeServer.get_num_ropes(), NativeRopeServer.get_computation_time() ] diff --git a/demo/rope_examples/scripts/animation_player.gd b/demo/rope_examples/scripts/animation_player.gd new file mode 100644 index 0000000..e0062ab --- /dev/null +++ b/demo/rope_examples/scripts/animation_player.gd @@ -0,0 +1,6 @@ +@tool +extends AnimationPlayer + + +func _ready() -> void: + play("moving") diff --git a/demo/rope_examples/scripts/character_body_2d.gd b/demo/rope_examples/scripts/character_body_2d.gd new file mode 100644 index 0000000..04ff130 --- /dev/null +++ b/demo/rope_examples/scripts/character_body_2d.gd @@ -0,0 +1,30 @@ +extends CharacterBody2D + +@export var use_arrow_keys: bool = false +@export var speed: float = 300.0 + + +func _physics_process(_delta: float) -> void: + var wishdir := Vector2() + + if use_arrow_keys: + if Input.is_key_pressed(KEY_LEFT): + wishdir.x -= 1 + if Input.is_key_pressed(KEY_RIGHT): + wishdir.x += 1 + if Input.is_key_pressed(KEY_UP): + wishdir.y -= 1 + if Input.is_key_pressed(KEY_DOWN): + wishdir.y += 1 + else: + if Input.is_key_pressed(KEY_A): + wishdir.x -= 1 + if Input.is_key_pressed(KEY_D): + wishdir.x += 1 + if Input.is_key_pressed(KEY_W): + wishdir.y -= 1 + if Input.is_key_pressed(KEY_S): + wishdir.y += 1 + + velocity = wishdir.normalized() * speed + move_and_slide() diff --git a/demo/rope_examples/scripts/character_body_2d_platformer.gd b/demo/rope_examples/scripts/character_body_2d_platformer.gd new file mode 100644 index 0000000..5ca8319 --- /dev/null +++ b/demo/rope_examples/scripts/character_body_2d_platformer.gd @@ -0,0 +1,63 @@ +extends CharacterBody2D + +@export var climp_speed: float = 1.0 +@export var rope_speed: float = 50.0 +@export var speed: float = 300.0 +@export var jump_speed: float = 500.0 +@export var gravity: float = 1200.0 + +var _rope_interaction: RopeInteraction + + +func _ready() -> void: + _rope_interaction = get_node_or_null("RopeInteraction") + + var area: Area2D = get_node_or_null("Area2D") + if area: + area.area_entered.connect(_rope_area_entered) + + +func _physics_process(delta: float) -> void: + var hdir := 0 + var on_rope := _rope_interaction.enable + var grounded: bool = is_on_floor() or on_rope + var move_speed := speed + + if Input.is_key_pressed(KEY_A): + hdir -= 1 + if Input.is_key_pressed(KEY_D): + hdir += 1 + + if on_rope: + var vdir := 0 + if Input.is_key_pressed(KEY_W): + vdir -= 1 + if Input.is_key_pressed(KEY_S): + vdir += 1 + + _rope_interaction.rope_position = clamp(_rope_interaction.rope_position + vdir * climp_speed * delta, 0.0, 1.0) + _rope_interaction.force_snap_to_rope() + move_speed = rope_speed + + if grounded: + velocity.y = 0 + + if Input.is_key_pressed(KEY_SPACE): + velocity += _rope_interaction.get_anchor().get_velocity() + velocity.y -= jump_speed + _rope_interaction.enable = false + + velocity.x = hdir * move_speed + velocity.y += gravity * delta + + move_and_slide() + + +func _rope_area_entered(area: Area2D) -> void: + # The rope area will have a collision generator which we use to get the actual rope node + var shape_generator := area.get_node("RopeCollisionShapeGenerator") as RopeCollisionShapeGenerator + var rope := shape_generator.get_node(shape_generator.rope_path) as Rope + _rope_interaction.rope = rope + _rope_interaction.enable = true + _rope_interaction.use_nearest_position() + _rope_interaction.force_snap_to_rope() diff --git a/demo/rope_examples/scripts/collision_detector.gd b/demo/rope_examples/scripts/collision_detector.gd new file mode 100644 index 0000000..c83065b --- /dev/null +++ b/demo/rope_examples/scripts/collision_detector.gd @@ -0,0 +1,17 @@ +@tool +extends Area2D + +@onready var indicator: Label = $indicator + +func _ready() -> void: + body_entered.connect(_body_entered) + body_exited.connect(_body_exited) + indicator.visible = false + + +func _body_entered(_body: Node2D) -> void: + indicator.visible = true + + +func _body_exited(_body: Node2D) -> void: + indicator.visible = false diff --git a/demo/rope_examples/stiffness.tscn b/demo/rope_examples/stiffness.tscn new file mode 100644 index 0000000..163368b --- /dev/null +++ b/demo/rope_examples/stiffness.tscn @@ -0,0 +1,59 @@ +[gd_scene load_steps=3 format=3 uid="uid://c7p2vp6s5nvem"] + +[ext_resource type="Script" path="res://addons/ropesim/RopeHandle.gd" id="1_flav6"] +[ext_resource type="Script" path="res://addons/ropesim/Rope.gd" id="4_ty6wv"] + +[node name="main" type="Node2D"] + +[node name="Without stiffness" type="Label" parent="."] +offset_left = 62.0 +offset_top = 160.0 +offset_right = 199.0 +offset_bottom = 183.0 +text = "Without stiffness:" + +[node name="RopeHandle" type="Marker2D" parent="."] +position = Vector2(117.049, 203.049) +rotation = 1.56586 +script = ExtResource("1_flav6") +rope_path = NodePath("../Rope") +strength = 1.0 + +[node name="Rope" type="Node2D" parent="."] +position = Vector2(96, 198.049) +rotation = -1.0472 +script = ExtResource("4_ty6wv") +num_segments = 20 +rope_length = 200.0 +metadata/_edit_group_ = true + +[node name="With stiffness" type="Label" parent="."] +offset_left = 288.0 +offset_top = 160.0 +offset_right = 398.0 +offset_bottom = 183.0 +text = "With stiffness:" + +[node name="Label" type="Label" parent="."] +offset_left = 18.0 +offset_top = 85.0 +offset_right = 599.0 +offset_bottom = 134.0 +text = "Stiffness forces the rope to return to its resting position. +The resting direction is downwards and affected by the the node's rotation." + +[node name="RopeHandle2" type="Marker2D" parent="."] +position = Vector2(343.049, 201.049) +rotation = 1.56586 +script = ExtResource("1_flav6") +rope_path = NodePath("../Rope2") +strength = 1.0 + +[node name="Rope2" type="Node2D" parent="."] +position = Vector2(322, 196.049) +rotation = -1.0472 +script = ExtResource("4_ty6wv") +num_segments = 20 +rope_length = 200.0 +stiffness = 12.145 +metadata/_edit_group_ = true diff --git a/demo/ropesim_demo.tscn b/demo/ropesim_demo.tscn deleted file mode 100644 index 88bd1de..0000000 --- a/demo/ropesim_demo.tscn +++ /dev/null @@ -1,53 +0,0 @@ -[gd_scene load_steps=6 format=3 uid="uid://n0h8fmj0t58s"] - -[ext_resource type="Script" path="res://addons/ropesim/RopeHandle.gd" id="1_3v13b"] -[ext_resource type="Script" path="res://addons/ropesim/RopeAnchor.gd" id="2_60osf"] -[ext_resource type="Texture2D" uid="uid://criwv6nuivcxy" path="res://icon.svg" id="3_t3x7v"] -[ext_resource type="Script" path="res://addons/ropesim/Rope.gd" id="4_ytdbs"] -[ext_resource type="Script" path="res://addons/ropesim/RopeRendererLine2D.gd" id="5_8a6rj"] - -[node name="main" type="Node2D"] - -[node name="RopeHandle" type="Marker2D" parent="."] -position = Vector2(161, 15) -script = ExtResource("1_3v13b") -rope_path = NodePath("../Rope") -rope_position = 0.62 - -[node name="RopeAnchor" type="Marker2D" parent="."] -position = Vector2(93.0733, 72.1881) -script = ExtResource("2_60osf") -rope_path = NodePath("../Rope") -rope_position = 0.25 - -[node name="Icon" type="Sprite2D" parent="RopeAnchor"] -position = Vector2(0, 32) -scale = Vector2(0.5, 0.5) -texture = ExtResource("3_t3x7v") - -[node name="RopeAnchor2" type="Marker2D" parent="."] -position = Vector2(155.257, 122.217) -script = ExtResource("2_60osf") -rope_path = NodePath("../Rope") - -[node name="Icon" type="Sprite2D" parent="RopeAnchor2"] -position = Vector2(0, 32) -scale = Vector2(0.5, 0.5) -texture = ExtResource("3_t3x7v") - -[node name="Rope" type="Node2D" parent="."] -texture_repeat = 2 -position = Vector2(295, 46) -script = ExtResource("4_ytdbs") -num_segments = 20 -rope_length = 200.0 -damping = 10.0 -render_line = false -metadata/_edit_group_ = true - -[node name="RopeRendererLine2D" type="Line2D" parent="Rope"] -show_behind_parent = true -points = PackedVector2Array(0, 0, 0, 10, 0, 20, 0, 30, 0, 40, 0, 50, 0, 60, 0, 70, 0, 80, 0, 90, 0, 100, 0, 110, 0, 120, 0, 130, 0, 140, 0, 150, 0, 160, 0, 170, 0, 180, 0, 190, 0, 200) -texture = ExtResource("3_t3x7v") -texture_mode = 1 -script = ExtResource("5_8a6rj") diff --git a/run_clang_tidy.sh b/run_clang_tidy.sh new file mode 100755 index 0000000..fdfcfb1 --- /dev/null +++ b/run_clang_tidy.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +cd "$(dirname "$(readlink -f "$0")")" || exit 1 + +clang-tidy src/*.cpp --config-file .clang-tidy -p . diff --git a/src/NativeRopeContext.cpp b/src/NativeRopeContext.cpp new file mode 100644 index 0000000..011f1a3 --- /dev/null +++ b/src/NativeRopeContext.cpp @@ -0,0 +1,202 @@ +#include "NativeRopeContext.hpp" + +using namespace godot; + +const Vector2 VECTOR_ZERO = Vector2(); +const Vector2 VECTOR_DOWN = Vector2(0, 1); + + +static float get_point_perc(int index, const PackedVector2Array& points) +{ + return (float)index / (points.size() > 0 ? float(points.size() - 1) : 0.f); +} + +static Vector2 damp_vec(Vector2 value, float damping_factor, double delta) +{ + return value.lerp(VECTOR_ZERO, (float)(1.0 - exp(-damping_factor * delta))); +} + + +void NativeRopeContext::_bind_methods() +{ +} + +bool NativeRopeContext::validate() const +{ + const int64_t size = points.size(); + return oldpoints.size() == size + && simulation_weights.size() == size + && seg_lengths.size() == size - 1; +} + +void NativeRopeContext::load_context(Node2D* rope) +{ + this->rope = rope; + points = rope->call("get_points"); + oldpoints = rope->call("get_old_points"); + damping_curve = rope->get("damping_curve"); + gravity = rope->get("gravity"); + gravity_direction = rope->get("gravity_direction"); + damping = rope->get("damping"); + stiffness = rope->get("stiffness"); + max_endpoint_distance = rope->get("max_endpoint_distance"); + num_constraint_iterations = rope->get("num_constraint_iterations"); + seg_lengths = rope->call("get_segment_lengths"); + simulation_weights = rope->get("_simulation_weights"); + fixate_begin = rope->get("fixate_begin"); +} + +void NativeRopeContext::simulate(double delta) +{ + if (points.size() < 2) + return; + + const float backup_multiplier_begin = simulation_weights[0]; + + if (fixate_begin) + { + simulation_weights[0] = 0; + points[0] = rope->get_global_position(); + } + + _simulate_velocities(delta); + _constraint(); + + if (fixate_begin) + simulation_weights[0] = backup_multiplier_begin; + + // PackedArrays are pass-by-reference in Godot 4.x but not when being passed to a C++ function. + // See https://github.com/godotengine/godot/pull/36492#issue-569558185. + rope->call("set_points", points); + rope->call("set_old_points", oldpoints); +} + +void NativeRopeContext::_simulate_velocities(double delta) +{ + const int first_idx = fixate_begin ? 1 : 0; + const int size = (int)points.size(); + + // NOTE: The following is a little ugly but it reduces memory usage by reusing existing buffers. + // It is also faster than allocating a new temporary buffer and also likely more cache efficient. + + // oldpoints is no longer needed after initial velocity computation, so we reuse it to store velocities. + PackedVector2Array& velocities = oldpoints; + // Afterwards we use oldpoints to store the new points and finally swap points and oldpoints + PackedVector2Array& new_points = oldpoints; + + // Compute velocities + for (int i = first_idx; i < size; ++i) + velocities[i] = points[i] - oldpoints[i]; + + // Stiffness + _simulate_stiffness(&velocities); + + // Apply velocity and damping + const float frame_gravity = (float)(gravity * delta); + const bool use_damping_curve = damping_curve.is_valid() && damping_curve->get_point_count() > 0; + + for (int i = first_idx; i < size; ++i) + { + const float dampmult = use_damping_curve ? (float)damping_curve->sample_baked(get_point_perc(i, points)) : 1.0f; + const Vector2 final_vel = simulation_weights[i] * ( + damp_vec(velocities[i], damping * dampmult, delta) + + gravity_direction * frame_gravity + ); + + new_points[i] = points[i] + final_vel; + } + + std::swap(oldpoints, points); +} + +void NativeRopeContext::_simulate_stiffness(PackedVector2Array* velocities) const +{ + // NOTE: oldpoints should not be used here, see comments in simulate_velocities(). + + if (stiffness <= 0) + return; + + Vector2 parent_seg_dir = rope->get_global_transform().basis_xform(VECTOR_DOWN).normalized(); + Vector2 last_stiffness_force; + + for (int i = 1; i < points.size(); ++i) + { + // NOTE: Asked a physicist to confirm this computation is physically accurate. + // He mentioned that, while it is technically correct, there is a material-dependent limit + // how far an object can bend before bending properties (stiffness) changes. + // E.g. a material might be less inclined to snap back into place at smaller bend angles, or + // a material might stop bending at some point, i.e. when it breaks. + // This implementation should probably suffice in most cases, but a more advanced + // implementation would include curves for stiffness in relation to segment position and + // for stiffness in relation to bend angle. + // + // | parent_seg_dir ---> parent_seg_dir.orthogonal() + // | \ + // V \ seg_dir + // \ seg_dir V + // \ + // V + const Vector2 seg_dir = (points[i] - points[i - 1]).normalized(); + const float angle = seg_dir.angle_to(parent_seg_dir); + + // The force directs orthogonal to the current segment + const Vector2 force_dir = seg_dir.orthogonal(); + + // Scale the force the further the segment bends. + // angle is signed and can be used to determine the force direction + last_stiffness_force += force_dir * (-angle / 3.1415f) * stiffness; + parent_seg_dir = seg_dir; + + // Update velocity + (*velocities)[i] += last_stiffness_force; + } +} + +static void constraint_segment(Vector2* point_a, Vector2* point_b, float weight_a, float weight_b, float seg_length) +{ + const Vector2 diff = *point_b - *point_a; + const float distance = diff.length(); + const float error = (seg_length - distance) * 0.5f; + const Vector2 dir = error * (diff / distance); + + // If one point has a weight < 1.0, the other point must compensate the difference in + // relation to its own weight. + // This is especially relevant with fixate_begin = true or with arbitrary weights = 0.0. + // In that case non-fixed point should be constrained by the whole error distance, not + // just half of it, because the other one can obviously not move. + // It actually works quite fine without this compensation, but this is more correct and + // produces better results. + *point_a -= (weight_a + weight_a * (1.0 - weight_b)) * dir; + *point_b += (weight_b + weight_b * (1.0 - weight_a)) * dir; +} + +void NativeRopeContext::_constraint() +{ + const bool use_euclid_constraint = max_endpoint_distance > 0; + Vector2* first_point; + Vector2* last_point; + float euclid_constraint_first_weight; + float max_stretch_length_sqr; + + if (use_euclid_constraint) + { + first_point = &points[0]; + last_point = &points[(int)points.size() - 1]; + euclid_constraint_first_weight = fixate_begin ? 0.0 : 1.0; + max_stretch_length_sqr = max_endpoint_distance * max_endpoint_distance; + } + + for (int _ = 0; _ < num_constraint_iterations; ++_) + { + if (use_euclid_constraint) + { + const float rope_length_sqr = first_point->distance_squared_to(*last_point); + + if (rope_length_sqr > max_stretch_length_sqr) + constraint_segment(first_point, last_point, euclid_constraint_first_weight, 1.0, max_endpoint_distance); + } + + for (int i = 0; i < points.size() - 1; ++i) + constraint_segment(&points[i], &points[i + 1], simulation_weights[i], simulation_weights[i + 1], seg_lengths[i]); + } +} diff --git a/src/NativeRopeContext.hpp b/src/NativeRopeContext.hpp new file mode 100644 index 0000000..320cc74 --- /dev/null +++ b/src/NativeRopeContext.hpp @@ -0,0 +1,43 @@ +#ifndef NATIVE_ROPE_CONTEXT_HPP +#define NATIVE_ROPE_CONTEXT_HPP + +#include "godot_cpp/classes/curve.hpp" +#include "godot_cpp/classes/node2d.hpp" + +namespace godot +{ + // Caches properties of a rope node and implements simulation functionality. + // TODO: Could be used as base class in the future to manage rope data in C++ and not in GDScript. + class NativeRopeContext : public Object + { + GDCLASS(NativeRopeContext, Object) // NOLINT + + public: + void load_context(Node2D* rope); + void simulate(double delta); + bool validate() const; + + protected: + static void _bind_methods(); + void _simulate_velocities(double delta); + void _simulate_stiffness(PackedVector2Array* velocities) const; + void _constraint(); + + public: + Node2D* rope; + PackedVector2Array points; + PackedVector2Array oldpoints; + PackedFloat32Array seg_lengths; + PackedFloat32Array simulation_weights; + float gravity; + Vector2 gravity_direction; + float damping; + float stiffness; + float max_endpoint_distance; + int num_constraint_iterations; + Ref damping_curve; + bool fixate_begin; + }; +} + +#endif diff --git a/src/NativeRopeServer.cpp b/src/NativeRopeServer.cpp index 18d5cc7..a274467 100644 --- a/src/NativeRopeServer.cpp +++ b/src/NativeRopeServer.cpp @@ -1,5 +1,7 @@ #include "NativeRopeServer.hpp" +#include "NativeRopeContext.hpp" #include "godot_cpp/classes/window.hpp" +#include "godot_cpp/core/class_db.hpp" #include #include #include @@ -10,24 +12,9 @@ using namespace godot; -const Vector2 VECTOR_ZERO = Vector2(); -const Vector2 VECTOR_DOWN = Vector2(0, 1); - - NativeRopeServer* NativeRopeServer::_singleton = nullptr; -float get_point_perc(int index, const PackedVector2Array& points) -{ - return index / (points.size() > 0 ? float(points.size() - 1) : 0.f); -} - -Vector2 damp_vec(Vector2 value, float damping_factor, float delta) -{ - return value.lerp(VECTOR_ZERO, 1.0 - exp(-damping_factor * delta)); -} - - NativeRopeServer::NativeRopeServer() : _tree(nullptr), _last_time(0.0), @@ -113,7 +100,7 @@ void NativeRopeServer::_start_stop_process() } _last_time = 0.f; - bool should_run = !_ropes.is_empty() && (!Engine::get_singleton()->is_editor_hint() || _update_in_editor); + const bool should_run = !_ropes.is_empty() && (!Engine::get_singleton()->is_editor_hint() || _update_in_editor); if (should_run != _is_running) { @@ -133,91 +120,26 @@ void NativeRopeServer::_start_stop_process() void NativeRopeServer::_on_physics_frame() { emit_signal("on_pre_update"); - double delta = _tree->get_root()->get_physics_process_delta_time(); - auto start = Time::get_singleton()->get_ticks_usec(); - - for (Node2D* rope : _ropes) - _simulate(rope, delta); + const double delta = _tree->get_root()->get_physics_process_delta_time(); + NativeRopeContext context; - _last_time = (Time::get_singleton()->get_ticks_usec() - start) / 1000.f; - emit_signal("on_post_update"); -} + const uint64_t start = Time::get_singleton()->get_ticks_usec(); -void NativeRopeServer::_simulate(Node2D* rope, float delta) -{ - PackedVector2Array points = rope->call("get_points"); - if (points.size() < 2) - return; - - PackedVector2Array oldpoints = rope->call("get_old_points"); - Ref damping_curve = rope->get("damping_curve"); - float gravity = rope->get("gravity"); - Vector2 gravity_direction = rope->get("gravity_direction"); - float damping = rope->get("damping"); - float stiffness = rope->get("stiffness"); - int num_constraint_iterations = rope->get("num_constraint_iterations"); - PackedFloat32Array seg_lengths = rope->call("get_segment_lengths"); - Vector2 parent_seg_dir = rope->get_global_transform().basis_xform(VECTOR_DOWN).normalized(); - Vector2 last_stiffness_force; - - // Simulate - for (size_t i = 1; i < points.size(); ++i) - { - Vector2 vel = points[i] - oldpoints[i]; - float dampmult = damping_curve.is_valid() ? damping_curve->sample_baked(get_point_perc(i, points)) : 1.0; - - // NOTE: Asked a physicist to confirm this computation is physically accurate. - // He mentioned that, while it is technically correct, there is a material-dependent limit - // how far an object can bend before bending properties (stiffness) changes. - // E.g. a material might be less inclined to snap back into place at smaller bend angles, or - // a material might stop bending at some point, i.e. when it breaks. - // This implementation should probably suffice in most cases, but a more advanced - // implementation would include curves for stiffness in relation to segment position and - // for stiffness in relation to bend angle. - if (stiffness > 0) - { - // | parent_seg_dir ---> parent_seg_dir.orthogonal() - // | \ - // V \ seg_dir - // \ seg_dir V - // \ - // V - Vector2 seg_dir = (points[i] - points[i - 1]).normalized(); - float angle = seg_dir.angle_to(parent_seg_dir); - - // The force directs orthogonal to the current segment - Vector2 force_dir = seg_dir.orthogonal(); - - // Scale the force the further the segment bends. - // angle is signed and can be used to determine the force direction - last_stiffness_force += force_dir * (-angle / 3.1415) * stiffness; - vel += last_stiffness_force; - parent_seg_dir = seg_dir; - } - - oldpoints.set(i, points[i]); - points.set(i, points[i] + damp_vec(vel, damping * dampmult, delta) + gravity_direction * gravity * delta); - } - - // Constraint - for (int _ = 0; _ < num_constraint_iterations; ++_) + for (Node2D* rope : _ropes) { - points.set(0, rope->get_global_position()); - points.set(1, points[0] + (points[1] - points[0]).normalized() * seg_lengths[0]); + context.load_context(rope); - for (size_t i = 1; i < points.size() - 1; ++i) + if (!context.validate()) [[unlikely]] { - Vector2 diff = points[i + 1] - points[i]; - float distance = diff.length(); - Vector2 dir = diff / distance; - float error = (seg_lengths[i] - distance) * 0.5; - points.set(i, points[i] - error * dir); - points.set(i + 1, points[i + 1] + error * dir); + UtilityFunctions::push_warning("Inconsistent rope data detected -> Skipped"); + continue; } + + context.simulate(delta); } - rope->call("set_points", points); - rope->call("set_old_points", oldpoints); + _last_time = (float)(Time::get_singleton()->get_ticks_usec() - start) / 1000.f; + emit_signal("on_post_update"); } float NativeRopeServer::get_computation_time() const diff --git a/src/NativeRopeServer.hpp b/src/NativeRopeServer.hpp index 303eb01..0f3160d 100644 --- a/src/NativeRopeServer.hpp +++ b/src/NativeRopeServer.hpp @@ -3,13 +3,14 @@ #include #include -#include "godot_cpp/templates/vector.hpp" +#include +#include namespace godot { class NativeRopeServer : public Object { - GDCLASS(NativeRopeServer, Object) + GDCLASS(NativeRopeServer, Object) // NOLINT public: NativeRopeServer(); @@ -31,7 +32,6 @@ namespace godot private: void _start_stop_process(); - void _simulate(Node2D* rope, float delta); void _on_physics_frame(); private: diff --git a/src/gdlibrary.cpp b/src/gdlibrary.cpp index 0bfe90e..3367df6 100644 --- a/src/gdlibrary.cpp +++ b/src/gdlibrary.cpp @@ -1,4 +1,5 @@ #include "gdlibrary.hpp" +#include "NativeRopeContext.hpp" #include "NativeRopeServer.hpp" #include @@ -17,7 +18,8 @@ void initialize_libropesim(ModuleInitializationLevel p_level) { } ClassDB::register_class(); - rope_server = memnew(NativeRopeServer); + ClassDB::register_class(); + rope_server = memnew(NativeRopeServer); // NOLINT Engine::get_singleton()->register_singleton("NativeRopeServer", rope_server); } @@ -34,7 +36,7 @@ void uninitialize_libropesim(ModuleInitializationLevel p_level) { extern "C" { // Initialization. GDExtensionBool GDE_EXPORT libropesim_init(GDExtensionInterfaceGetProcAddress p_get_proc_address, const GDExtensionClassLibraryPtr p_library, GDExtensionInitialization *r_initialization) { - godot::GDExtensionBinding::InitObject init_obj(p_get_proc_address, p_library, r_initialization); + const godot::GDExtensionBinding::InitObject init_obj(p_get_proc_address, p_library, r_initialization); init_obj.register_initializer(initialize_libropesim); init_obj.register_terminator(uninitialize_libropesim);