├── .gitattributes ├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── TestScene.tscn ├── distance_joint_2d.gd ├── icon.svg ├── icon.svg.import ├── moving_anchor.gd └── project.godot /.gitattributes: -------------------------------------------------------------------------------- 1 | # Normalize EOL for all files that Git considers text files. 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: dastmo 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Godot 4+ specific ignores 2 | .godot/ 3 | /android/ 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Godot DistanceJoint2D 2 | A simple 2D distance joint for Godot that is not a spring. 3 | 4 | This script does not allow the creation of double/triple/etc. pendulums and that is by design. Rigidbody2Ds will behave as if linked by a string that does have some mass. 5 | 6 | [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/L4L2128QIJ) 7 | 8 | ## Quickstart 9 | 1. Copy the distance_joint_2d.gd file to your project. 10 | 2. Instantiate a DistanceJoint2D node in the scene where you want to use it. 11 | 3. Assign nodes to the DistanceJoint2D in one of two ways: 12 | * Assign a pivot (any Node2D) and add as many Rigidbody2D nodes to the links array as you wish. 13 | * Assign at least two Rigidbody2D nodes to the links array, without a pivot. 14 | 15 | ## Properties 16 | 17 | |Property|Type|Description|Default Value| 18 | |--------|----|-----------|-------------| 19 | |disable_collision|bool|Disables collision between all nodes in the system (pivot and links)|true| 20 | |pivot|NodePath|Assigns a static pivot to the system, creating an anchor for the chain of links to hang from. If the pivot is a Rigidbody2D, it will be frozen on joint initialization.|""| 21 | |links|Array[Rigidbody2D]|The array that holds the Rigidbody2D nodes that need to be in the system. They will be linked to each other in the order they are added to the array.|N/A| 22 | |auto_distance|bool|If true, the maximum distance between the links will be calculated from their position at the time of joint initialization. total_distance will be the sum of all the distances between links. This allows for the distances between links to not be uniform. If false, the distance between links will be uniform and based on the total_distance of the joint.|false| 23 | |total_distance|float|The total allowed distance between the pivot (or first link if no pivot is set) and the last link in the system. Changing it at runtime will have no effect if auto_distance is true.| 24 | |joint_type|JointType (enum)|The type of the joint. If set to CHAIN, links will be linked in a chain one after the other. If set to ORBIT, they will orbit the pivot or common centerpoint (if no pivot is assigned.)|JointType.CHAIN| 25 | |fixed_distance|bool|If TRUE, the joint will try to keep a fixed distance between the the links.|false| 26 | 27 | ## Known Issues 28 | * When having multiple links and a pivot assigned, the second link in the chain will jitter a lot. 29 | * Currently, if fixed_distance is true, the joint overstretches, akin to a spring. Needs improvement. 30 | 31 | ## Planned features 32 | * Mode that allows the creation of chaotic pendulums. 33 | -------------------------------------------------------------------------------- /TestScene.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=9 format=3 uid="uid://dkf2gjln7bweg"] 2 | 3 | [ext_resource type="Texture2D" uid="uid://byl7ysneaas0l" path="res://icon.svg" id="1_hhcti"] 4 | [ext_resource type="Script" path="res://distance_joint_2d.gd" id="2_miprn"] 5 | [ext_resource type="Script" path="res://moving_anchor.gd" id="3_35alp"] 6 | 7 | [sub_resource type="RectangleShape2D" id="RectangleShape2D_0r5ih"] 8 | size = Vector2(60, 60) 9 | 10 | [sub_resource type="RectangleShape2D" id="RectangleShape2D_osnvf"] 11 | size = Vector2(32, 32) 12 | 13 | [sub_resource type="RectangleShape2D" id="RectangleShape2D_2xwh5"] 14 | size = Vector2(120, 120) 15 | 16 | [sub_resource type="RectangleShape2D" id="RectangleShape2D_7e88c"] 17 | size = Vector2(30, 30) 18 | 19 | [sub_resource type="RectangleShape2D" id="RectangleShape2D_v0eof"] 20 | size = Vector2(2000, 20) 21 | 22 | [node name="TestScene" type="Node2D"] 23 | 24 | [node name="DistanceJoint2D4" type="Node2D" parent="." node_paths=PackedStringArray("links")] 25 | script = ExtResource("2_miprn") 26 | pivot = NodePath("../MovingPivot") 27 | links = [NodePath("../RigidBody2D4")] 28 | total_distance = 200.0 29 | 30 | [node name="MovingPivot" type="CharacterBody2D" parent="." node_paths=PackedStringArray("joint")] 31 | position = Vector2(143, 132) 32 | script = ExtResource("3_35alp") 33 | joint = NodePath("../DistanceJoint2D4") 34 | 35 | [node name="Sprite2D" type="Sprite2D" parent="MovingPivot"] 36 | scale = Vector2(0.5, 0.5) 37 | texture = ExtResource("1_hhcti") 38 | 39 | [node name="CollisionShape2D" type="CollisionShape2D" parent="MovingPivot"] 40 | shape = SubResource("RectangleShape2D_0r5ih") 41 | 42 | [node name="RigidBody2D4" type="RigidBody2D" parent="."] 43 | position = Vector2(140, 238) 44 | 45 | [node name="CollisionShape2D" type="CollisionShape2D" parent="RigidBody2D4"] 46 | shape = SubResource("RectangleShape2D_osnvf") 47 | 48 | [node name="Sprite2D" type="Sprite2D" parent="RigidBody2D4"] 49 | scale = Vector2(0.25, 0.25) 50 | texture = ExtResource("1_hhcti") 51 | 52 | [node name="DistanceJoint2D" type="Node2D" parent="." node_paths=PackedStringArray("links")] 53 | position = Vector2(57, 67) 54 | script = ExtResource("2_miprn") 55 | pivot = NodePath("../StaticBody2D") 56 | links = [NodePath("../RigidBody2D"), NodePath("../RigidBody2D2"), NodePath("../RigidBody2D3"), NodePath("../RigidBody2D7")] 57 | total_distance = 100.0 58 | joint_type = 1 59 | 60 | [node name="StaticBody2D" type="StaticBody2D" parent="."] 61 | position = Vector2(865, 147) 62 | 63 | [node name="Sprite2D" type="Sprite2D" parent="StaticBody2D"] 64 | texture = ExtResource("1_hhcti") 65 | 66 | [node name="CollisionShape2D" type="CollisionShape2D" parent="StaticBody2D"] 67 | shape = SubResource("RectangleShape2D_2xwh5") 68 | 69 | [node name="RigidBody2D" type="RigidBody2D" parent="."] 70 | position = Vector2(859, 254) 71 | 72 | [node name="CollisionShape2D" type="CollisionShape2D" parent="RigidBody2D"] 73 | shape = SubResource("RectangleShape2D_osnvf") 74 | 75 | [node name="Sprite2D" type="Sprite2D" parent="RigidBody2D"] 76 | scale = Vector2(0.25, 0.25) 77 | texture = ExtResource("1_hhcti") 78 | 79 | [node name="RigidBody2D2" type="RigidBody2D" parent="."] 80 | position = Vector2(784, 312) 81 | 82 | [node name="CollisionShape2D" type="CollisionShape2D" parent="RigidBody2D2"] 83 | shape = SubResource("RectangleShape2D_osnvf") 84 | 85 | [node name="Sprite2D" type="Sprite2D" parent="RigidBody2D2"] 86 | scale = Vector2(0.25, 0.25) 87 | texture = ExtResource("1_hhcti") 88 | 89 | [node name="RigidBody2D3" type="RigidBody2D" parent="."] 90 | position = Vector2(850, 428) 91 | 92 | [node name="CollisionShape2D" type="CollisionShape2D" parent="RigidBody2D3"] 93 | shape = SubResource("RectangleShape2D_osnvf") 94 | 95 | [node name="Sprite2D" type="Sprite2D" parent="RigidBody2D3"] 96 | scale = Vector2(0.25, 0.25) 97 | texture = ExtResource("1_hhcti") 98 | 99 | [node name="RigidBody2D7" type="RigidBody2D" parent="."] 100 | position = Vector2(914, 516) 101 | 102 | [node name="CollisionShape2D" type="CollisionShape2D" parent="RigidBody2D7"] 103 | shape = SubResource("RectangleShape2D_osnvf") 104 | 105 | [node name="Sprite2D" type="Sprite2D" parent="RigidBody2D7"] 106 | scale = Vector2(0.25, 0.25) 107 | texture = ExtResource("1_hhcti") 108 | 109 | [node name="DistanceJoint2D2" type="Node2D" parent="." node_paths=PackedStringArray("links")] 110 | script = ExtResource("2_miprn") 111 | disable_collision = false 112 | links = [NodePath("../RigidBody2D8"), NodePath("../RigidBody2D5"), NodePath("../RigidBody2D9")] 113 | auto_distance = true 114 | 115 | [node name="StaticBody2D2" type="StaticBody2D" parent="."] 116 | position = Vector2(475, 259) 117 | 118 | [node name="Sprite2D" type="Sprite2D" parent="StaticBody2D2"] 119 | texture = ExtResource("1_hhcti") 120 | 121 | [node name="CollisionShape2D" type="CollisionShape2D" parent="StaticBody2D2"] 122 | shape = SubResource("RectangleShape2D_2xwh5") 123 | 124 | [node name="RigidBody2D5" type="RigidBody2D" parent="."] 125 | position = Vector2(569, 369) 126 | 127 | [node name="CollisionShape2D" type="CollisionShape2D" parent="RigidBody2D5"] 128 | shape = SubResource("RectangleShape2D_7e88c") 129 | 130 | [node name="Sprite2D" type="Sprite2D" parent="RigidBody2D5"] 131 | scale = Vector2(0.25, 0.25) 132 | texture = ExtResource("1_hhcti") 133 | 134 | [node name="RigidBody2D8" type="RigidBody2D" parent="."] 135 | position = Vector2(474, 125) 136 | 137 | [node name="CollisionShape2D" type="CollisionShape2D" parent="RigidBody2D8"] 138 | shape = SubResource("RectangleShape2D_7e88c") 139 | 140 | [node name="Sprite2D" type="Sprite2D" parent="RigidBody2D8"] 141 | scale = Vector2(0.25, 0.25) 142 | texture = ExtResource("1_hhcti") 143 | 144 | [node name="RigidBody2D9" type="RigidBody2D" parent="."] 145 | position = Vector2(596, 427) 146 | 147 | [node name="CollisionShape2D" type="CollisionShape2D" parent="RigidBody2D9"] 148 | shape = SubResource("RectangleShape2D_7e88c") 149 | 150 | [node name="Sprite2D" type="Sprite2D" parent="RigidBody2D9"] 151 | scale = Vector2(0.25, 0.25) 152 | texture = ExtResource("1_hhcti") 153 | 154 | [node name="StaticBody2D3" type="StaticBody2D" parent="."] 155 | position = Vector2(722, 634) 156 | 157 | [node name="CollisionShape2D" type="CollisionShape2D" parent="StaticBody2D3"] 158 | shape = SubResource("RectangleShape2D_v0eof") 159 | 160 | [node name="StaticBody2D4" type="StaticBody2D" parent="."] 161 | position = Vector2(1146, 490) 162 | rotation = -1.56836 163 | 164 | [node name="CollisionShape2D" type="CollisionShape2D" parent="StaticBody2D4"] 165 | shape = SubResource("RectangleShape2D_v0eof") 166 | 167 | [node name="StaticBody2D5" type="StaticBody2D" parent="."] 168 | position = Vector2(11.9998, 487) 169 | rotation = -1.56836 170 | 171 | [node name="CollisionShape2D" type="CollisionShape2D" parent="StaticBody2D5"] 172 | shape = SubResource("RectangleShape2D_v0eof") 173 | -------------------------------------------------------------------------------- /distance_joint_2d.gd: -------------------------------------------------------------------------------- 1 | extends Node2D 2 | class_name DistanceJoint2D 3 | 4 | 5 | @export var disable_collision: bool = true 6 | @export var pivot: NodePath = NodePath("") 7 | @export var links: Array[RigidBody2D] 8 | @export var auto_distance: bool = false 9 | @export var total_distance: float = 0.0 10 | @export var joint_type: JointType = JointType.CHAIN 11 | @export var fixed_distance: bool = false 12 | 13 | 14 | var _pivot: Node2D 15 | var _joint_valid: bool = true 16 | var _link_distances: Array[float] = [] 17 | 18 | 19 | # Variables used to monitor for changes 20 | var _previous_disable_collision: bool 21 | var _previous_pivot: NodePath 22 | var _previous_links: Array[RigidBody2D] 23 | var _previous_auto_distance: bool 24 | var _previous_total_distance: float 25 | var _previous_joint_type: JointType 26 | var _previous_fixed_distance: bool 27 | 28 | 29 | enum JointType {CHAIN, ORBIT} 30 | 31 | 32 | func _ready() -> void: 33 | _recalculate_joint() 34 | 35 | 36 | func _recalculate_joint() -> void: 37 | _previous_disable_collision = disable_collision 38 | _previous_pivot = pivot 39 | _previous_links = links.duplicate() 40 | _previous_auto_distance = auto_distance 41 | _previous_joint_type = joint_type 42 | _previous_fixed_distance = fixed_distance 43 | 44 | if pivot != NodePath(""): 45 | _pivot = get_node(pivot) 46 | else: 47 | _pivot = null 48 | 49 | _joint_valid = _validate_joint() 50 | if not _joint_valid: 51 | return 52 | 53 | _set_initial_distances() 54 | 55 | if disable_collision: 56 | _add_collision_exceptions() 57 | else: 58 | _remove_collision_exceptions() 59 | 60 | for link in links: 61 | link.can_sleep = false 62 | 63 | 64 | func _physics_process(delta: float) -> void: 65 | if _check_for_changes(): 66 | _recalculate_joint() 67 | 68 | _apply_constraints(delta) 69 | 70 | 71 | func _validate_joint() -> bool: 72 | if links.size() == 0: 73 | push_warning("No Rigidbody2D added to the links array. Joint is invalid.") 74 | return false 75 | 76 | if links.size() < 2 and not is_instance_valid(_pivot): 77 | push_warning("Add at least two links or one link and one pivot. Joint is invalid.") 78 | return false 79 | 80 | if _pivot is RigidBody2D: 81 | if _pivot.freeze == false: 82 | _pivot.freeze = true 83 | push_warning("Rigidbody2D static pivot frozen by joint.") 84 | 85 | return true 86 | 87 | 88 | func _add_collision_exceptions() -> void: 89 | if _pivot is PhysicsBody2D: 90 | for link in links: 91 | _pivot.add_collision_exception_with(link) 92 | 93 | for link in links: 94 | for other_link in links: 95 | if other_link != link: 96 | link.add_collision_exception_with(other_link) 97 | 98 | 99 | func _remove_collision_exceptions() -> void: 100 | if _pivot is PhysicsBody2D: 101 | for link in links: 102 | _pivot.remove_collision_exception_with(link) 103 | 104 | for link in links: 105 | for other_link in links: 106 | if other_link != link: 107 | link.remove_collision_exception_with(other_link) 108 | 109 | 110 | func _set_initial_distances() -> void: 111 | _link_distances.clear() 112 | 113 | if joint_type == JointType.CHAIN: 114 | if not auto_distance: 115 | _set_uniform_distances() 116 | else: 117 | _set_distances() 118 | elif joint_type == JointType.ORBIT: 119 | _set_orbit_distances() 120 | 121 | _previous_total_distance = total_distance 122 | 123 | _set_links_initial_positions() 124 | 125 | 126 | func _set_uniform_distances() -> void: 127 | if is_instance_valid(_pivot): 128 | var _links_distance: float = total_distance / links.size() 129 | for i in links.size(): 130 | _link_distances.append(_links_distance) 131 | else: 132 | var _links_distance: float = total_distance / (links.size() - 1) 133 | for i in links.size(): 134 | if i == 0: 135 | _link_distances.append(0.0) 136 | else: 137 | _link_distances.append(_links_distance) 138 | 139 | 140 | func _set_distances() -> void: 141 | total_distance = 0.0 142 | for i in links.size(): 143 | if i == 0: 144 | if is_instance_valid(_pivot): 145 | _link_distances.append(links[i].global_position.distance_to(_pivot.global_position)) 146 | else: 147 | _link_distances.append(0.0) 148 | else: 149 | _link_distances.append(links[i].global_position.distance_to(links[i - 1].global_position)) 150 | 151 | for distance in _link_distances: 152 | total_distance += distance 153 | 154 | 155 | func _set_orbit_distances() -> void: 156 | if auto_distance: 157 | total_distance = 0.0 158 | var center: Vector2 = _get_orbit_center() 159 | 160 | for i in links.size(): 161 | _link_distances.append(links[i].global_position.distance_to(center)) 162 | else: 163 | for i in links.size(): 164 | _link_distances.append(total_distance) 165 | 166 | 167 | func _apply_constraints(delta: float) -> void: 168 | if not _joint_valid: 169 | return 170 | 171 | if joint_type == JointType.CHAIN and not fixed_distance: 172 | _apply_chain_constraint(delta) 173 | elif joint_type == JointType.CHAIN and fixed_distance: 174 | _apply_fixed_chain_constraint(delta) 175 | elif joint_type == JointType.ORBIT and not fixed_distance: 176 | _apply_orbit_constraint(delta) 177 | elif joint_type == JointType.ORBIT and fixed_distance: 178 | _apply_fixed_orbit_constraint(delta) 179 | 180 | 181 | func _apply_chain_constraint(delta: float) -> void: 182 | for i in links.size(): 183 | if i == 0 and is_instance_valid(_pivot): 184 | var _next_pos: Vector2 = links[i].global_position + (links[i].linear_velocity * delta) 185 | var _next_distance = 0.0 186 | _next_distance = _next_pos.distance_to(_pivot.global_position) 187 | 188 | if _next_distance <= _link_distances[i]: 189 | continue 190 | 191 | var _v: Vector2 = Vector2.ZERO 192 | _v = _next_pos.direction_to(_pivot.global_position) * (_next_distance - _link_distances[i]) 193 | 194 | links[i].linear_velocity += _v / delta 195 | else: 196 | var _next_pos = links[i].global_position + links[i].linear_velocity * delta 197 | var _next_distance_previous = 0.0 198 | var _next_distance_next = 0.0 199 | 200 | if i == 0: 201 | _next_distance_next = _next_pos.distance_to(links[i + 1].global_position) 202 | elif i == links.size() - 1: 203 | _next_distance_previous = _next_pos.distance_to(links[i - 1].global_position) 204 | else: 205 | _next_distance_next = _next_pos.distance_to(links[i + 1].global_position) 206 | _next_distance_previous = _next_pos.distance_to(links[i - 1].global_position) 207 | 208 | 209 | if i < links.size() - 1: 210 | if _next_distance_next > _link_distances[i + 1]: 211 | var _distance = _link_distances[i + 1] - _next_distance_next 212 | links[i].linear_velocity -= ( 213 | _next_pos.direction_to(links[i+1].global_position) * 214 | _distance / 215 | delta 216 | ) 217 | if i > 0: 218 | if _next_distance_previous > _link_distances[i]: 219 | var _distance = _link_distances[i] - _next_distance_previous 220 | links[i].linear_velocity -= ( 221 | _next_pos.direction_to(links[i-1].global_position) * 222 | _distance / 223 | delta 224 | ) 225 | 226 | 227 | func _apply_fixed_chain_constraint(delta: float) -> void: 228 | for i in links.size(): 229 | if i == 0 and is_instance_valid(_pivot): 230 | var _next_pos: Vector2 = links[i].global_position + (links[i].linear_velocity * delta) 231 | var _next_distance = 0.0 232 | _next_distance = _next_pos.distance_to(_pivot.global_position) 233 | 234 | var _v: Vector2 = Vector2.ZERO 235 | if _next_distance > _link_distances[i]: 236 | _v = _next_pos.direction_to(_pivot.global_position) * (_next_distance - _link_distances[i]) 237 | elif _next_distance < _link_distances[i]: 238 | _v = _pivot.global_position.direction_to(_next_pos) * (_link_distances[i] - _next_distance) 239 | 240 | links[i].linear_velocity += _v / delta 241 | else: 242 | var _next_pos = links[i].global_position + links[i].linear_velocity * delta 243 | var _next_distance_previous = 0.0 244 | var _next_distance_next = 0.0 245 | 246 | if i == 0: 247 | _next_distance_next = _next_pos.distance_to(links[i + 1].global_position) 248 | elif i == links.size() - 1: 249 | _next_distance_previous = _next_pos.distance_to(links[i - 1].global_position) 250 | else: 251 | _next_distance_next = _next_pos.distance_to(links[i + 1].global_position) 252 | _next_distance_previous = _next_pos.distance_to(links[i - 1].global_position) 253 | 254 | 255 | if i < links.size() - 1: 256 | if _next_distance_next > _link_distances[i + 1]: 257 | var _distance = _link_distances[i + 1] - _next_distance_next 258 | links[i].linear_velocity -= ( 259 | _next_pos.direction_to(links[i+1].global_position) * 260 | _distance 261 | ) 262 | elif _next_distance_next < _link_distances[i + 1]: 263 | var _distance = _next_distance_next - _link_distances[i + 1] 264 | links[i].linear_velocity -= ( 265 | links[i+1].global_position.direction_to(_next_pos) * 266 | _distance 267 | ) 268 | if i > 0: 269 | if _next_distance_previous > _link_distances[i]: 270 | var _distance = _link_distances[i] - _next_distance_previous 271 | links[i].linear_velocity -= ( 272 | _next_pos.direction_to(links[i-1].global_position) * 273 | _distance 274 | ) 275 | elif _next_distance_previous < _link_distances[i]: 276 | var _distance = _next_distance_previous - _link_distances[i] 277 | links[i].linear_velocity -= ( 278 | links[i-1].global_position.direction_to(_next_pos) * 279 | _distance 280 | ) 281 | 282 | 283 | func _apply_orbit_constraint(delta: float) -> void: 284 | var center: Vector2 = _get_orbit_center() 285 | 286 | for i in links.size(): 287 | var _next_pos: Vector2 = links[i].global_position + (links[i].linear_velocity * delta) 288 | var _next_distance = 0.0 289 | _next_distance = _next_pos.distance_to(center) 290 | 291 | if _next_distance <= _link_distances[i]: 292 | continue 293 | 294 | var _v: Vector2 = Vector2.ZERO 295 | _v = _next_pos.direction_to(center) * (_next_distance - _link_distances[i]) 296 | 297 | links[i].linear_velocity += _v / delta 298 | 299 | 300 | func _apply_fixed_orbit_constraint(delta: float) -> void: 301 | var center: Vector2 = _get_orbit_center() 302 | 303 | for i in links.size(): 304 | var _next_pos: Vector2 = links[i].global_position + (links[i].linear_velocity * delta) 305 | var _next_distance = 0.0 306 | _next_distance = _next_pos.distance_to(center) 307 | 308 | if _next_distance == _link_distances[i]: 309 | continue 310 | 311 | var _v: Vector2 = Vector2.ZERO 312 | 313 | if _next_distance > _link_distances[i]: 314 | _v = _next_pos.direction_to(center) * (_next_distance - _link_distances[i]) 315 | elif _next_distance < _link_distances[i]: 316 | _v = center.direction_to(_next_pos) * (_link_distances[i] - _next_distance) 317 | 318 | links[i].linear_velocity += _v / delta 319 | 320 | 321 | func _get_orbit_center() -> Vector2: 322 | var center: Vector2 = Vector2.ZERO 323 | 324 | if is_instance_valid(_pivot): 325 | center = _pivot.global_position 326 | else: 327 | for link in links: 328 | center += link.global_position 329 | center = center / links.size() 330 | 331 | return center 332 | 333 | 334 | func _set_links_initial_positions() -> void: 335 | for i in links.size(): 336 | var _dir: Vector2 337 | if i == 0 and is_instance_valid(_pivot): 338 | _dir = _pivot.global_position.direction_to(links[i].global_position) 339 | links[i].global_position = _pivot.global_position + (_dir * _link_distances[i]) 340 | elif i > 0: 341 | _dir = links[i - 1].global_position.direction_to(links[i].global_position) 342 | links[i].global_position = links[i - 1].global_position + (_dir * _link_distances[i]) 343 | 344 | 345 | func _check_for_changes() -> bool: 346 | if disable_collision != _previous_disable_collision: 347 | return true 348 | 349 | if pivot != _previous_pivot: 350 | return true 351 | 352 | if links.hash() != _previous_links.hash(): 353 | return true 354 | 355 | if auto_distance != _previous_auto_distance: 356 | return true 357 | 358 | if total_distance != _previous_total_distance: 359 | return true 360 | 361 | if joint_type != _previous_joint_type: 362 | return true 363 | 364 | if fixed_distance != _previous_fixed_distance: 365 | return true 366 | 367 | return false 368 | -------------------------------------------------------------------------------- /icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icon.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://byl7ysneaas0l" 6 | path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://icon.svg" 14 | dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /moving_anchor.gd: -------------------------------------------------------------------------------- 1 | extends CharacterBody2D 2 | 3 | 4 | @export var joint: DistanceJoint2D 5 | 6 | 7 | var input: Vector2 = Vector2.ZERO 8 | var movement_speed: float = 100.0 9 | 10 | 11 | func _process(delta: float) -> void: 12 | input = Vector2.ZERO 13 | if Input.is_key_pressed(KEY_D): 14 | input += Vector2.RIGHT 15 | if Input.is_key_pressed(KEY_A): 16 | input += Vector2.LEFT 17 | if Input.is_key_pressed(KEY_S): 18 | input += Vector2.DOWN 19 | if Input.is_key_pressed(KEY_W): 20 | input += Vector2.UP 21 | if Input.is_key_pressed(KEY_UP): 22 | joint.total_distance -= (50.0 * delta) 23 | if Input.is_key_pressed(KEY_DOWN): 24 | joint.total_distance += (50.0 * delta) 25 | if Input.is_key_pressed(KEY_SPACE) and joint.links.size() > 0: 26 | joint.links.remove_at(0) 27 | if Input.is_key_pressed(KEY_SHIFT): 28 | joint.pivot = NodePath("") 29 | 30 | 31 | func _physics_process(_delta: float) -> void: 32 | velocity = input * movement_speed 33 | move_and_slide() 34 | -------------------------------------------------------------------------------- /project.godot: -------------------------------------------------------------------------------- 1 | ; Engine configuration file. 2 | ; It's best edited using the editor UI and not directly, 3 | ; since the parameters that go here are not all obvious. 4 | ; 5 | ; Format: 6 | ; [section] ; section goes between [] 7 | ; param=value ; assign values to parameters 8 | 9 | config_version=5 10 | 11 | [application] 12 | 13 | config/name="DistanceJoint2D" 14 | config/features=PackedStringArray("4.3", "Mobile") 15 | config/icon="res://icon.svg" 16 | 17 | [rendering] 18 | 19 | renderer/rendering_method="mobile" 20 | --------------------------------------------------------------------------------