+2 votes

I want to create a segmented enemy similar to the Chain Chomps from Mario.

enter image description here

There is a an unmoving base segment, a head, and a chain of segments between the the head and the base. The head is constrained to some distance from the base, and the segments between the head and the base undulate based on the movement of the head.

I know how to create distance constraints so that the segments follow the head. Basically, you can just get the vector from each segment to the segment in front of it, and if the distance is greater than some value, you can move the segment some fraction of the difference toward the segment in front of it.

The problem is I don't know how to take into account the unmoving base. I imagine I would need to doubly constrain each segment so that they also link to the unmoving base, but I don't know how.

I know it should be possible to achieve this kind of enemy using the built-in joints of Godot, but ideally I'd like to code the constraints myself.

Any advice on how to create this kind of enemy?

in Engine by (1,549 points)
edited by

Okay after some experiments I realized this is one of those things that can be as complex as you want to, with chains being able to be lifted in the center by giving each link a «prev» and «next» value and then sending a force along the chain in both directions reducing the force for every step it takes including gravity, wind and multiple points of pull etc.

Anyway, the quickest version of this would probably instead be to look at the first and last node as a chain of only 2. And then do the tail as a separate chain only for the visual effect. Maybe even via a shader if you want to get into that.

1 Answer

+3 votes
Best answer

Lucky for you I have some experience in constraints solving. Assuming your chain chomp local scene tree looks something like this,
Chain chomp local scene tree
Then the code for your chain chomp would go as follows.

extends StaticBody2D

Constants

const NUMBER_OF_LINKS = 4
const CHAIN_LENGTH = 200
const DIST_BETWEEN_LINKS = CHAIN_LENGTH / NUMBER_OF_LINKS

const ITERATIONS = 10
const GRAVITY = Vector2(0, 300)

The first group of constants define the size of the chain.
NUMBER_OF_LINKS must be the same as the number of chain sprites in the tree.
ITERATIONS defines the number of times we solve the constraints in one frame. The bigger the number is the better.
The rest are self explanatory.


Variable initialization

var prev_link_positions = []
var links = []

func _ready():
    for i in range(NUMBER_OF_LINKS):
        links.append(get_child(i))
        prev_link_positions.append(get_child(i).position)

I prefer to use verlet integration when working with physics constraints. So we'll have to store an array of vectors which hold the chain links' previous positions. The second array holds reference to the chain link nodes.
They all then get initialized in the _ready method.


func _physics_process(delta):

Force applying & Verlet integration

    for i in range(links.size()):
        var acceleration = GRAVITY
        acceleration *= delta * delta
        var prev_position = links[i].position

        links[i].position = 2*links[i].position - prev_link_positions[i] + acceleration
        prev_link_positions[i] = prev_position

We apply a constant gravity force to each chain link and update their position and previous position.


Constraint solving

    for iteration in range (ITERATIONS):
        for i in range(NUMBER_OF_LINKS):
            var link = links[i]     
    var prev_link_position = Vector2() if i == 0 else links[i-1].position
            var next_link_position = $Head.position if i == NUMBER_OF_LINKS-1 else links[i+1].position
            var vec_to_link

            vec_to_link = prev_link_position - link.position
            link.position += vec_to_link.normalized() * max(vec_to_link.length() - DIST_BETWEEN_LINKS, 0) / 2.0

            vec_to_link = next_link_position - link.position
            link.position += vec_to_link.normalized() * max(vec_to_link.length() - DIST_BETWEEN_LINKS, 0) / 2.0

We solve the constraints on each link several times. The constraints here keep the links connected to the base, head and each other.

Overall the code looks like this.

extends StaticBody2D

const NUMBER_OF_LINKS = 4
const CHAIN_LENGTH = 200
const DIST_BETWEEN_LINKS = CHAIN_LENGTH / NUMBER_OF_LINKS

const ITERATIONS = 10
const GRAVITY = Vector2(0, 300)

var prev_link_positions = []
var links = []

func _ready():
    for i in range(NUMBER_OF_LINKS):
        links.append(get_child(i))
        prev_link_positions.append(get_child(i).position)

func _physics_process(delta):

   # You add code to move the head here.

    for i in range(links.size()):
        var acceleration = GRAVITY
        acceleration *= delta * delta
        var prev_position = links[i].position

        links[i].position = 2*links[i].position - prev_link_positions[i] + acceleration

        prev_link_positions[i] = prev_position
    for iteration in range (ITERATIONS):
        for i in range(NUMBER_OF_LINKS):
            var link = links[i]     
    var prev_link_position = Vector2() if i == 0 else links[i-1].position
            var next_link_position = $Head.position if i == NUMBER_OF_LINKS-1 else links[i+1].position
            var vec_to_link

            vec_to_link = prev_link_position - link.position
            link.position += vec_to_link.normalized() * max(vec_to_link.length() - DIST_BETWEEN_LINKS, 0) / 2.0

            vec_to_link = next_link_position - link.position
            link.position += vec_to_link.normalized() * max(vec_to_link.length() - DIST_BETWEEN_LINKS, 0) / 2.0
by (3,875 points)
selected by

Brilliant explanation, thanks so much!

Welcome to Godot Engine Q&A, where you can ask questions and receive answers from other members of the community.

Please make sure to read How to use this Q&A? before posting your first questions.
Social login is currently unavailable. If you've previously logged in with a Facebook or GitHub account, use the I forgot my password link in the login box to set a password for your account. If you still can't access your account, send an email to webmaster@godotengine.org with your username.