Chain-like Animation Tutorial

Introduction

This tutorial is basically a showcase of my attempts to create a chain-like animation. In my usual style, it will be written on a dummy-level. I hope you won't be annoyed with that.

About what kind of animation am I talking about? It's a style of animation like on this screenshot(the dragon).

This kind of animation also appears in The Griffon Legend, and Josiah Tobin referred to it, in his review of The Griffon Legend, as bio-animation, though I failed to find it under that name on the net. My google skills are appalling. 

So allow me to call this chain-like animation, since it really functions like a chain. Another valid descriptions could be string-like animation too, because these kinds of animations are often more similar to a string connecting beads than to a chain.

Let's just say we want to create a snake consisted of [insert number] blobs and be able to move it around the screen. The blobs should be a part of one chain/string and move in that manner.

Before all, we should decide how we want to declare our chain(s). My idea is to declare an array defined with two elements, first representing the chain number and second every specific ring(piece, blob, bead, whatever) of that chain.

Like this:

const numofchains = 10
const numofrings = 50

DIM SHARED MyChain(numofchains, numofrings) AS MyChainType

numofchains will represent the maximum possible number of chains that can be simultaneously active and you don't have to declare it like a constant. numofrings will represent the maximum number of rings(pieces) each chain can feature. Again, you don't have to declare it like a constant. I could have done it like this too:

DIM SHARED MyChain(10, 50) AS MyChainType

But declaring these numbers as constants allows us more flexibility, in case we want to connect them with certain FOR loops and be able to change our game/program parameters faster. With a chain declared on this way we must have in mind that managing a chain piece MyChain(10, 51), MyChain (11, 20) or any other out of bounds will result in shit hitting the fan. If you want your game to feature chains consisted of 100+ pieces simply change the numofrings constant accordingly.

Before "DIM SHARED MyChain(numofchains, numofrings) AS MyChainType" we need to define the "MyChainType" user defined type. This is how I constructed it:

TYPE MyChainType
X AS SINGLE
Y AS SINGLE
OldX AS SINGLE
OldY AS SINGLE
Sprite AS INTEGER
Mode AS INTEGER
Exists AS INTEGER
Length AS INTEGER
PullDistance AS INTEGER
EndDistance AS INTEGER
Fixed AS INTEGER
Speed AS SINGLE
END TYPE

Some of these variables don't make much sense now but will become later. X and Y speak for themselves, Sprite will represent the sprite that makes the piece/ring, Exists if a chain piece is active or not, PullDistance will define the distance on which one piece starts to pull another, EndDistance the distance on which one peace will prevent the other to move any further in case one of the pieces is fixed(a chain with a ball hanging on the wall). Speed will define the speed of movement in pixels of every chain piece.

Before all let's set our graphic mode:

SCREENRES 640, 480, 8, 2, 0
SCREENSET 1, 0
SETMOUSE 50,50,0

After than we should set the number of chains and rings per chain in our specific program:

set_num_of_chains = 1
set_num_of_rings = 25

Make these variables as shared. I decided to have one chain consisted of 25 pieces/blobs. Like I already said, it will represent a snake.

Before working with the chain we need to initiate proper values of every piece in the chain:

FOR initchain = 1 TO set_num_of_chains
FOR initring = 1 TO set_num_of_rings
MyChain(initchain, initring).Exists = TRUE    
MyChain(initchain, initring).Sprite = 1
IF initring = set_num_of_rings THEN MyChain(initchain, initring).Sprite = 2
MyChain(initchain, initring).PullDistance = 12
MyChain(initchain, initring).EndDistance = 14
MyChain(initchain, initring).Speed = 2
MyChain(initchain, initring).X = 320
MyChain(initchain, initring).Y = 20 + initring * 7
NEXT initring
NEXT initchain

This initiates a chain on the horizontal center of the screen with every new chain piece being 7 pixels more close to the bottom of the screen(MyChain(initchain, initring).Y = 20 + initring * 7). Note the pixel "distance" variables I picked. And note how the last chain piece is of different look(sprite variable) since it will represent the snake's head.

My loops usually look like how it follows so I'll use the same in this program:

DO

screenset workpage, workpage xor 1

CLS

MyChainLayer

workpage xor = 1

do 
loop until timer& - st >= (1/fpslimit) 
st = timer& 

SLEEP 1

LOOP UNTIL MULTIKEY(SC_ESCAPE)


Be sure to declare fpslimit as a constant with:

const fpslimit = 80

Where fpslimit will define the speed of our program in FPS.

Also, you should declare workpage and st like this:

DIM SHARED workpage
DIM SHARED st AS DOUBLE
 
Don't forget to declare the usual TRUE/FALSE constants too:

const FALSE = 0
const TRUE = 1

MyChainLayer is a subroutine we need to declare with:

DECLARE SUB MyChainLayer ()

This sub will include all the code that connects the chain pieces into a chain.

After the loop the code goes:

SUB MyChainLayer ()

' Loop through our chains and chain pieces of
' every chain.
FOR countchain = 1 TO set_num_of_chains
FOR countring = 1 TO set_num_of_rings

' Store the chain piece position before moving it.
MyChain(countchain, countring).OldX = MyChain(countchain, countring).X
MyChain(countchain, countring).OldY = MyChain(countchain, countring).Y

' If not moving the last chain piece(IF countring + 1 <= set_num_of_rings)
' and if the current piece is not fixed move it!
IF countring + 1 <= set_num_of_rings AND MyChain(countchain, countring).Fixed = FALSE THEN
' If the next chain piece(countring+1) is more than PullDistance pixels left from
' the current chain piece(countring), move the current chain piece toward left.
IF MyChain(countchain, countring).X-MyChain(countchain, countring+1).X>MyChain(countchain, countring).PullDistance THEN
MyChain(countchain, countring).X = MyChain(countchain, countring).X-MyChain(countchain, countring).Speed
' Align the current piece with the next piece it follows, if it's higher 
' or lower from this piece. This part is tweaked in the sense of
' "alignment speed" and requires tweaking with speed changes.
IF MyChain(countchain, countring).Y>MyChain(countchain, countring+1).Y THEN MyChain(countchain, countring).Y = MyChain(countchain, countring).Y-ABS(MyChain(countchain, countring).Y-MyChain(countchain, countring+1).Y)*0.08*MyChain(countchain, countring).Speed
IF MyChain(countchain, countring).Y<MyChain(countchain, countring+1).Y THEN MyChain(countchain, countring).Y = MyChain(countchain, countring).Y+ABS(MyChain(countchain, countring).Y-MyChain(countchain, countring+1).Y)*0.08*MyChain(countchain, countring).Speed
END IF
' If the next chain piece is more than PullDistance pixels right from
' the current chain piece, move the current chain piece toward right.
IF MyChain(countchain, countring).X-MyChain(countchain, countring+1).X<-MyChain(countchain, countring).PullDistance THEN
MyChain(countchain, countring).X = MyChain(countchain, countring).X+MyChain(countchain, countring).Speed
IF MyChain(countchain, countring).Y>MyChain(countchain, countring+1).Y THEN MyChain(countchain, countring).Y = MyChain(countchain, countring).Y-ABS(MyChain(countchain, countring).Y-MyChain(countchain, countring+1).Y)*0.08*MyChain(countchain, countring).Speed
IF MyChain(countchain, countring).Y<MyChain(countchain, countring+1).Y THEN MyChain(countchain, countring).Y = MyChain(countchain, countring).Y+ABS(MyChain(countchain, countring).Y-MyChain(countchain, countring+1).Y)*0.08*MyChain(countchain, countring).Speed
END IF
' If the next chain piece is more than PullDistance pixels up from
' the current chain piece, move the current chain piece toward up.
IF MyChain(countchain, countring).Y-MyChain(countchain, countring+1).Y>MyChain(countchain, countring).PullDistance THEN
MyChain(countchain, countring).Y = MyChain(countchain, countring).Y-MyChain(countchain, countring).Speed
' Align the current piece with the next piece it follows, if it's left 
' or right from this piece. This part is tweaked in the sense of
' "alignment speed" and requires tweaking with speed changes.
IF MyChain(countchain, countring).X>MyChain(countchain, countring+1).X THEN MyChain(countchain, countring).X = MyChain(countchain, countring).X-ABS(MyChain(countchain, countring).X-MyChain(countchain, countring+1).X)*0.08*MyChain(countchain, countring).Speed
IF MyChain(countchain, countring).X<MyChain(countchain, countring+1).X THEN MyChain(countchain, countring).X = MyChain(countchain, countring).X+ABS(MyChain(countchain, countring).X-MyChain(countchain, countring+1).X)*0.08*MyChain(countchain, countring).Speed
END IF
' If the next chain piece is more than PullDistance pixels down from
' the current chain piece, move the current chain piece toward down.
IF MyChain(countchain, countring).Y-MyChain(countchain, countring+1).Y<-MyChain(countchain, countring).PullDistance THEN
MyChain(countchain, countring).Y = MyChain(countchain, countring).Y+MyChain(countchain, countring).Speed
IF MyChain(countchain, countring).X>MyChain(countchain, countring+1).X THEN MyChain(countchain, countring).X = MyChain(countchain, countring).X-ABS(MyChain(countchain, countring).X-MyChain(countchain, countring+1).X)*0.08*MyChain(countchain, countring).Speed
IF MyChain(countchain, countring).X<MyChain(countchain, countring+1).X THEN MyChain(countchain, countring).X = MyChain(countchain, countring).X+ABS(MyChain(countchain, countring).X-MyChain(countchain, countring+1).X)*0.08*MyChain(countchain, countring).Speed
END IF
END IF

' If a chain piece exists draw it!
' According to the piece sprite draw a specific kind 
' of blob. In a proper game CIRCLE statements should 
' be replaced with PUT and memory buffers holding the
' blob sprites should be connected with
' MyChain(countchain, countring).Sprite.
' Sprite = 1 ' green blob
' Sprite = 2 ' red blob(snake's head)
IF MyChain(countchain, countring).Exists = TRUE THEN
IF MyChain(countchain, countring).Sprite = 1 THEN
CIRCLE (MyChain(countchain, countring).X, MyChain(countchain, countring).Y), 15, 2, , , , F    
CIRCLE (MyChain(countchain, countring).X, MyChain(countchain, countring).Y), 15, 118
LINE (MyChain(countchain, countring).X-5,MyChain(countchain, countring).Y-5)-(MyChain(countchain, countring).X-4,MyChain(countchain, countring).Y-6), 47
END IF
IF MyChain(countchain, countring).Sprite = 2 THEN
CIRCLE (MyChain(countchain, countring).X, MyChain(countchain, countring).Y), 15, 40, , , , F    
CIRCLE (MyChain(countchain, countring).X, MyChain(countchain, countring).Y), 15, 4
LINE (MyChain(countchain, countring).X-5,MyChain(countchain, countring).Y-5)-(MyChain(countchain, countring).X-4,MyChain(countchain, countring).Y-6), 43
END IF
END IF
    
NEXT countring
NEXT countchain

END SUB

The code mostly speaks for itself. The only thing that might confuse you is the alignment of pieces as the current piece follows the next one. If you would REM these alignment lines you would get crap. I'll try to explain the alignment speed like this. Imagine the snake moving left or right while the pieces are aligned vertically. In this specific case, the alignment speed represents the speed of pieces aligning with one another horizontally, since the snake's head is moving horizontally. I devised this type of formula(vertical alignment):

IF MyChain(countchain, countring).Y>MyChain(countchain, countring+1).Y THEN MyChain(countchain, countring).Y = MyChain(countchain, countring).Y-ABS(MyChain(countchain, countring).Y-MyChain(countchain, countring+1).Y)*0.08*MyChain(countchain, countring).Speed

As you see the very speed of alignment is in:

ABS(MyChain(countchain, countring).Y-MyChain(countchain, countring+1).Y)*0.08*MyChain(countchain, countring).Speed

And depends on the absolute distance of two blobs vertically and the global speed of blobs. It's all multiplied with 0.08, with different numbers giving different results. Let's call that number the alignment factor. Lower numbers make the snake too stiff while with higher numbers the curves the snake makes are too sharp.  Try it yourself. Also, the lower the global speed is, the effect is better. So higher FPS values are advisable since then you can use lower pixel speeds.

The entire code so far: codever1.txt

Compile this and you'll get a snake on the screen. Yay! But this is no good to us if we can't move the head or connect it with some movement algorithm. So just add this code in the main loop after 'MyChainLayer':

IF MULTIKEY(SC_RIGHT) THEN MyChain(1, set_num_of_rings).X = MyChain(1, set_num_of_rings).X + MyChain(1, set_num_of_rings).Speed
IF MULTIKEY(SC_LEFT) THEN MyChain(1, set_num_of_rings).X = MyChain(1, set_num_of_rings).X - MyChain(1, set_num_of_rings).Speed
IF MULTIKEY(SC_DOWN) THEN MyChain(1, set_num_of_rings).Y = MyChain(1, set_num_of_rings).Y + MyChain(1, set_num_of_rings).Speed
IF MULTIKEY(SC_UP) THEN MyChain(1, set_num_of_rings).Y = MyChain(1, set_num_of_rings).Y - MyChain(1, set_num_of_rings).Speed

IF ABS(MyChain(1, set_num_of_rings).X-MyChain(1, set_num_of_rings-1).X)>MyChain(1, set_num_of_rings).EndDistance THEN MyChain(1, set_num_of_rings).X = MyChain(1, set_num_of_rings).OldX
IF ABS(MyChain(1, set_num_of_rings).Y-MyChain(1, set_num_of_rings-1).Y)>MyChain(1, set_num_of_rings).EndDistance THEN MyChain(1, set_num_of_rings).Y = MyChain(1, set_num_of_rings).OldY

This moves the last chain piece in chain 1 according to pressed arrow key. The last two
lines prevent the head to move any further if you make one chain piece fixed, which we'll do later.

Compile the code again and move the snake. You should get results like this:

snake_moves1.gif

Change the number of rings if you like, or the distance between them. This is the experiement I did with PullDistance of 22 and alingment factor of 0.04.

snake_moves2.gif

Now, what if one chain piece, preferably the first one, is fixed/constrained to a, let's say, body of a monster. The next changes allow us to use our chain-like animation code for that very kind of situations.

At the end of chain initiation just put:

MyChain(1, 1).Fixed = TRUE 

This flags that the chain 1, ring 1 is fixed. Only two extra lines are needed and put them inside MyChainLayer, before 'IF MyChain(countchain, countring).Exists = TRUE THEN':


IF countring > 1 AND ABS(MyChain(countchain, countring).X-MyChain(countchain, countring-1).X)>MyChain(countchain, countring).EndDistance THEN MyChain(countchain, countring).X = MyChain(countchain, countring).OldX
IF countring > 1 AND ABS(MyChain(countchain, countring).Y-MyChain(countchain, countring-1).Y)>MyChain(countchain, countring).EndDistance THEN MyChain(countchain, countring).Y = MyChain(countchain, countring).OldY


They restore the position of a chain piece if it goes more than EndDistance from a chain piece preceding it, which goes all the way to the root(number 1) chain-piece. I recommend you to REM these lines if you are not going to use fixed chain pieces.

The altered code: codever2.txt

Compile it, pull the snake down as much as possible and them move left/right. This should result in a desired effect, the one you would like to have in a game.

Don't be afraid to experiment with the distance variables and 0.08 value to suit your needs.

I must admit that the code related to fixed chain pieces is far from satisfactory, but here ends the scope of my method. I also tried to create an example program with metal balls hanging from the ceiling and swinging using this code, but I couldn't get them behave on a desirable way. I could mock it up but that wouldn't be this method.

Hopefully, some of you will find this code insightful when working on a project featuring chain-line animations.

If you construct something better and more versatile please share it with the community. I know this can be done much better.






