Tutorial (Godot Engine v3 - GDScript) - Verlet Chain (v0.01)!

in #utopian-io2 years ago (edited)

Godot Engine Logo v3 (Beginner).png Tutorial

...learn about Verlet Chains!

What Will I Learn?

Hi,

I posted a tutorial at the weekend that demonstrated both Verlet Integration and Godot's RigidBody physics. At the end of the tutorial, I mentioned the fact that I had developed a chain and rope movements, which can also be applied to bridges etc.

This Tutorial will explain how to create Chains, dangling from a ceiling, using Verlet Integration; I plan to do the same with Godot Physics in a future post.

Why have I bothered? I have several game ideas that require chain and rope type effects, so I thought it would be wise to prototype it, so that I can implement it in a game without the fear of not knowing how to achieve it!

Writing games can often be a journey down the research and experimentation path! Which, in itself, can lead to new ideas and discovery; so I find it fun!

This is what we are going to build:


chains.gif

Note: the recording method does so at 30fps

, therefore, it ruins the quality throughout this article!

NOTE: This is the first iteration of the code, therefore it is NOT perfect! I.E. as you can see in the video above, the movement of the chain appears to grow with momentum, when actually, it shouldn't become so extreme! This will be ironed out in a future post.


Assumptions

You will

  • Overview
  • Add a Chain Loop instance
  • Add a Chain Link instance
  • Add the Chain instance
  • Add control to the Loop!

Requirements

You must have installed Godot Engine v3.0.

All the code from this tutorial is provided in a GitHub repository. I'll explain more about this, towards the end of the tutorial. You may want to download this


Overview

A simulated chain is very easy to implement, in fact, so is a rope because it is implemented in the same way!

Effectively, we need to build a list of objects, that when attached to each other, force them to interact. We are going to use Verlet Integration, which ensures a list of objects have physics applied to them, given they have a vector of movement.

The objects we are going to create have been termed for my purposes, NOT, by some scientific formula way. If you go looking at Wikipedia, you can soon get lost in lots of algorithms, symbols and mathematical meaning. I'm not here to fully explain the maths! IN fact, refer to Coding Math youtube channel tutorial which does a FAR better job than I!

The terms I'm going to use are:

  • Chain Loop: This is the side-on chain ring Loop.png
  • Chain Loop Anchor: The fixed, non-movable Chain Loop Anchor.png
  • Chain Link: The side of chain link Link.png

I've seen many GREAT art assets, that provide really convincing chains. However, to simulate the moving, they need to be broken into their constituent parts; allowing them to be overlayed to build a chain.

Building the chain is very simple:

  • Plot each Loop up to its Size + Gap distance from the next
  • Overlay a Link, exactly halfway between the two Loops and ensure it rotated at the angle between them

_Note: It is important to note the starting angles of objects, i.e. 0 degrees faces the right, therefore the link object must be horizontal to being with.

All physics shall be applied to every Loop object and the Link object shall restrict the loops, as per the Point / Sticks metaphor in the Coding Maths tutorial.

Let's get on with it!

Add a Chain Loop instance

A Chain Loop is the first object to create, therefore, please create a new Instance and add a Sprite as the Root node.

I then assign the Loop image into the texture, which is a 24 x 24 pixel circle.

Please attach a Script and enter the following:

extends Sprite

Extend the Sprite Class

const CHAIN_LOOP_GAP = 2

Define a gap between the Loops (play with this when the code is running, what is important to remember? Hint, think about the links)

export var anchor = false

Define an exposed flag (for the editor), to be able to define this loop as an Anchor point. I will discuss this further down, in the Chain section.

var pos = Vector2()
var oldPos = Vector2()

Allocate local pos and oldPos variables. The Sprite already has a position and global_position properties, but we need these two variables to calculate vectors each frame. In the previous tutorial, these were held in the main code (i.e. our Chain below); however, I felt moving them into the Loop object makes both more sense and keeps the other cleaner!

func setPosition(index):
    pos = Vector2(0, index + 1) 
    pos *= (texture.get_size().y + CHAIN_LOOP_GAP)
    oldPos = pos
    position = pos

This defines a new function that will position the Loop on screen, based on a position index number. I.E. Each loop is placed below the last. This again could be kept in the Chain object, but for me, logically it makes sense to keep it inside the object that is going to use it.

The function sets the pos class variable to the index position (plus one to be lower than the Anchor) and then multiplies it by the size and gap required per Loop
The pos value is also placed into the oldPos and into the position property; forcing the Loop to a position on the screen

Make sure you save the Scene and script, I placed them into a folder structure of /Chains/Loop/*

You can add the instance to the Game Scene, and you should be able to see a Loop; nothing exciting so far!

  • Add a Chain Link instance
    The Chain Link is even less exciting! Create a new instance with a Sprite root Node named Chain Link. Add the texture, ensuring it is lying horizontally:

image.png

As stated previously, angle 0 faces Right, therefore the link must face right because it will be rotated to match the angle between two Loops.

Finally, add a script:

extends Sprite

var parentLoop = null
var childLoop = null

This again, extends a Sprite Class

The two points that would normally be kept in a dictionary of the main code has been moved into the object instead, this, in my opinion, maintains clarity as to the purposes of the variables. These two simply point to the parent and child loop; the parent is the Loop above and the child is the Loop below.

image.png

I saved mine in the folder /Chain/Link/*

All done! Two down, one object to go...

  • Add the Chain instance
    The Chain instance is where ALL the magic occurs. It brings the Loops and Links together to form a beautiful chain.

Please add a new instance with a Node2D as the root Node, named Chain. We use Node2D, because the 'Chain' can be placed anywhere on the Screen, but is not a Sprite itself.

Please add an instance of the Loop as a child, which we are going to call AnchorLoop. We are reusing the Loop object, to form the Anchor:

image.png

In the Inspector, override the Loop image texture with the Anchor Loop image (it has the fixed connector on top).

image.png

We also enable the 'Anchor' property, that was exposed! The Chain code will use this flag to ensure it keeps it stationary; i.e. it won't apply movement to it. This is another example of why I feel placing this type of property in the object is better practice than keeping in a list of dictionary items that get lost! Also, any other script can look up this node uses its condition.

Please attach a Script to the Chain Node2D. This is the most involved script of all:

extends Node2D

const CHAIN_LOOP = preload("res://Chain/Loop/ChainLoop.tscn")
const CHAIN_LINK = preload("res://Chain/Link/ChainLink.tscn")

const CHAIN_LINK_LENGTH = 26

const GRAVITY = 0.6
const RESISTANCE = 0.98
const FRICTION = 0.90

var loops = []
var links = []

export (int) var linkCount = 10

onready var SCREEN_SIZE = get_viewport().size

Extend the Node2D class

Declare preloaded constants for the Loop and Link instances; allowing them to be instanced

Declare the chain length (24 pixels) + 2 for the Gap

Declare the physics properties, including Gravity, air Resistance and Friction

Declare two arrays for the Loops and Links (to accelerate the code, rather than finding objects in the tree))

Declare an exposed variable for the count of links to create; this can be set in the editor, allowing for differing lengths to be 'grown'

Finally, obtain the screen size from the viewport (although this needs to be a fetch of the screen size in the ProjectSettings if 2D stretching is enabled).

Next, we build the chain when the Node is ready:

func _ready():
    addAnchorLoop()
    for i in range (linkCount):
        addLoop(i)
        addLink(i)

Add the Anchor object (already declared in the tree) to the Loop array

Then loop for the number of links configured, adding a Loop and then a Link instance

func addAnchorLoop():
    loops.append($AnchorLoop)

The function to add the Anchor to the Loops array. This function isn't necessary per say, as it could have been placed inline in the ready function; however, I like to create a specific meaning to my code. This is my preference. Given this is initialisation code, performance is NOT a factor, readability is.

func addLoop(index):
    var loop = CHAIN_LOOP.instance()
    loop.setPosition(index)
    loops.append(loop)
    add_child(loop)

For the next 'loop' index, create the Chain Loop, set its position (via the instance's internal function, see above) and then both append it the array and place it on the screen

func addLink(index):
    var link = CHAIN_LINK.instance()
    link.parentLoop = loops[index]
    link.childLoop = loops[index + 1]
    links.append(link)
    add_child(link)

Do the same as we did with the add loop function, creating the Link and setting the parent and child Loop points; remember, the Anchor will have been added as index 0, hence, the childhood is + 1.

Next, we declare the next frame process function:

func _process(delta):
    updateLoops()
    constrainLinks()
    renderFrame()

For each frame, update the Loops (i.e. apply physics), then constrain their positions by the links and finally render them

FYI, I experimented with the idea of implementing the physics inside each Loop node itself; DONT! After doing so, I recognised the bad decisions I made:

  • There is no guarantee on the order of processing by Godot Engine; for the chains, we need the order to be processed top down!
  • It is slow! Using a fixed array is far faster than Godot to processing the process function of each node; because that has an overhead.
  • We perform a constrain after applying the physics. We can do both of those before changing the final position of the Sprite! However, if the logic is placed inside the Node Script, they have to position and reposition themselves, which looks both ugly and jittery!

It is far better to process the main loop as we have it. This will be important for the "Space Invaders" clone!

func updateLoops():
    for loop in loops:
        if !loop.anchor:
            applyMovement(loop)

func applyMovement(loop):
    var velocity = calcVelocity(loop)
    loop.oldPos = loop.pos
    loop.pos += velocity
    loop.pos.y += GRAVITY

func calcVelocity(loop):
    return (loop.pos - loop.oldPos) * RESISTANCE * FRICTION

The update of the Loops is constructed through three functions:

  1. The first loops through each Loop and calls the movement function, if it isn't the Anchor Loop (as stated previously, we have to prevent it from moving; unless it is an engine! I.E. an Engine might move it side to side!
  2. The second function applies the physics to the Node. The velocity is first calculated (via the the third function). The current position is moved into the old position, and then both the velocity and gravity forces are added to the current position.
  3. Calculates the velocity, using the old and current positions.

func constrainLinks():
for link in links:
var vector = calcLinkVector(link)
var distance = link.childLoop.pos.distance_to(link.parentLoop.pos)
var difference = CHAIN_LINK_LENGTH - distance
var percentage = difference / distance
vector *= percentage
link.childLoop.pos += vector

The update loops function constrains the Loops, by calling this function.

It loops through all the links and calculates the vector between the Parent and the Child (see next function).

It then calculates the distance between the Parent and Child positions.

It then calculates the difference between the distance and the Link length.

A percentage can then be calculated, which is then applied to the original vector between then Parent and Child. The adjustment is applied to the Child position, forcing it towards the Parent, maintaining the correct distance. Please refer to the Coding Maths for an explanation of this.

func calcLinkVector(link):
    return link.childLoop.pos - link.parentLoop.pos

This function is used by the previous to calculate the vector between the child and parent positions.

The last of the update Loops function calls the render frame function:

func renderFrame():
    for link in links:
        link.childLoop.position = link.childLoop.pos
        positionLinkBetweenLoops(link)
        rotateLinkBetweenLoops(link)

Each link is worked through, with its child Loop positioned on screen (the parent is ignored because the first link will refer to the Anchor; which won't move. Then, the child of this link becomes the parent of the next, which has already been moved!)

The next step is to position the Link, inbetween the Parent and Child Loops, see the next function

Finally, the Link is rotated by the second function below

func positionLinkBetweenLoops(link):
    link.position = link.parentLoop.pos + (calcLinkVector(link) / 2)

Positioning the Link between the Loops is relatively straightforward. Calculate the vector between the two Loops, divide it in half and add it to the Parent Loop position.

func rotateLinkBetweenLoops(link):
    link.global_rotation = link.parentLoop.pos.angle_to_point(link.childLoop.pos)

Rotating the Link is also trivial because Godot provides a function on Vector2 that calculates the 'Angle to point' between the two.

Add a chain to the Game Scene and run it!


image.png

... oh!! They show, but there is no movment, nor a way to interact!

Add control to the Loop!

We can easily remedy this issue, but providing mouse/touch control to the Chain Loop instance! There is a Control node, which detects when the mouse/touch occurs on it; i.e. enter or exits. Let's use this.

Open the Chain Loop instance. Add a Control as a parent:

image.png

Ensure you set the Inspector/Rect/Position values so that the control overlaps the image in the 2D View:

image.png

I had to set position to (-12, -12):

image.png

Next, the on entered action of the control needs to be wired to the Script of the ChainLoop instance. Open the Node tab of the Inspector:

image.png

Double click the mouse_entered() function and then select the ChainLoop instance:

image.png

Click the Connect button at the bottom of the page and the Script will open with a new function. Please change it to:

func _on_Control_mouse_entered():
    if !anchor:
        over = true
        modulate = Color(0x0000ffff)

If the Loop Node is not the anchor, set the Class variable over to true (this needs to be added, see next line of code) and then we set the Sprite's colour to Blue

var over = false

Please add this variable to the top of the script. It is used to flag when the user is interacting with the Loop Node.

Do the same for the mouse_exited() function and change the code to this:

func _on_Control_mouse_exited():
    if !anchor:
        over = false
        modulate = Color(0xffffffff)

This resets the over flag and ensures the Loop sprite is returned to its original colour (i.e. the mouse has moved away from the Sprite Control!)

func _process(delta):
    if Input.is_mouse_button_pressed(BUTTON_LEFT) and over:
        var mousePos = get_viewport().get_mouse_position()
        var vector = mousePos - global_position
        oldPos = pos
        pos += vector

The above process function is required to process the Loop every frame to determine whether the mouse is over it and the Left Button has been pressed. If it has, calculate the vector from the Loop Node to the Mouse and then move it towards it! It is important the local pos is used and NOT the Sprites position because the Chain code will ensure the Link restrain is applied (clever, yes?)

If you run the game now, interaction is enabled, BUT, when you move and let go of a selected Node, it seems to "jerk" awkwardly; that's because Godot Engine maybe in the middle of the Chain movement logic. We need to alter the Chain function:

func updateLoops():
    for loop in loops:
        if !loop.anchor _and !loop.over_:
            applyMovement(loop)

The additional 'and condition' is applied, only allowing movement to occur to a Node if it does not have a mouse over it! This will smooth the movement after the user releases the button.

Try running! It should mirror my example, as seen in the animated Gif at the top of this tutorial!

Finally

This tutorial has shown you how to implement a Verlet Chain! I hope it is useful and that someone might use it in a game; i have a few ideas of my own.

There are some improvements to make:

  • Prevent the Chains to whip around too quickly. One option is to use the angles of the previous loop and the next to ensure a threshold is not crossed; i.e. the link would restrict movement, because the chain would hit itself

  • It would be good for the chain loops and links above a user moved loop to also move, i.e. so the entire chain would subtly move

I will look into this in the future, and will provide a v1.00; unless someone here figures it, then let me know!

My next task is to provide this as a RigidBody solution using Godot's Joints!

I'm also going to use this approach for the 'Space Invader' clone; i.e. for the ship formations and to direct them. Have a google on Spring Joints!!!

Please do comment and ask questions! I'm more than happy to interact with you.

Sample Project

I hope you've read through this Tutorial, as it will provide you with the hands-on skills that you simply can not learn from downloading the sample set of code.

However, for those wanting the code, please download from GitHub.

You should then Import the "Verlet Chain Swing" folder into Godot Engine.

Other Tutorials

Beginners

Competent



Posted on Utopian.io - Rewarding Open Source Contributors

Sort:  

Thank you for the contribution. It has been approved.

You can contact us on Discord.
[utopian-moderator]

Hey @roj, I just gave you a tip for your hard work on moderation. Upvote this comment to support the utopian moderators and increase your future rewards!

Thank you Roj; especially for the effort it must take to read so many of these!

Congratulations @sp33dy, this post is the third most rewarded post (based on pending payouts) in the last 12 hours written by a User account holder (accounts that hold between 0.1 and 1.0 Mega Vests). The total number of posts by User account holders during this period was 2973 and the total pending payments to posts in this category was $5526.98. To see the full list of highest paid posts across all accounts categories, click here.

If you do not wish to receive these messages in future, please reply stop to this comment.

Wow, that is prett amazing! I'm stoked by that revelation! ..and I've only just begun posting :)

Hey @sp33dy I am @utopian-io. I have just upvoted you!

Achievements

  • You have less than 500 followers. Just gave you a gift to help you succeed!
  • Seems like you contribute quite often. AMAZING!

Community-Driven Witness!

I am the first and only Steem Community-Driven Witness. Participate on Discord. Lets GROW TOGETHER!

mooncryption-utopian-witness-gif

Up-vote this comment to grow my power and help Open Source contributions like this one. Want to chat? Join me on Discord https://discord.gg/Pc8HG9x