Back in late 2021 I got to work on building out a full action platformer game for mobile platforms. My game, Shadow of the Orient, is loosely based on history and incorporates fictional enemies like dragons and Yeti’s. When I was working on the dragon character I wanted it to move around like a snake because that’s the way it should move but i really had no idea how to pull it off…at first. After giving it some thought I started putting the pieces together and the following video sample was the end result:
Structure Overview
The structure of the dragon is created with a master parent gameObject followed by each body part as a child gameObject as seen in the following reference image:
The reason for this structure is to allow the entire dragon to move around in any direction while having the body parts behave independently regardless of the direction the parent gameObject is moving. The “dragon_head” gameObject is responsible for controlling the vertical movement using a script which calculates a wave motion and each body part which follows the dragon’s head is controlled through an additional script. The following are the two scripts which I coded to get the dragon to move that I way wanted:
WaveMovement.cs
The following code is assigned to the “dragon_head” gameObject and is responsible for controlling the vertical motion of the dragon – it also allows for horizontal motion if desired.
using System.Collections; using System.Collections.Generic; using UnityEngine; public enum WaveMovementDirection { horizontal, vertical } public class WaveMovement : MonoBehaviour { [SerializeField] private bool playOnStart = false; [SerializeField] private bool disableEventCall = false; [Header("Wave Settings")] [SerializeField] private Transform _parentTransform; [SerializeField] private WaveMovementDirection waveDirection; [SerializeField] private float maxWave = 10f; [Header("Movement Settings")] [SerializeField] private bool moveObject = false; [SerializeField] private float moveSpeed = 0f; [SerializeField] private Vector2 moveDirection; //Internal vars private bool _canMove = false; private float _waveNum = 0f; private float _yMotion = 0f; private float _xMotion = 0f; private Vector3 _movementVector; private Transform _transform; private float _xPos; private float _yPos; private void Awake() { //Cache the transform _transform = transform; } // Start is called before the first frame update void Start() { //Check if the motion should play on start if (playOnStart) _canMove = true; } // Update is called once per frame void FixedUpdate() { //Check if we can move if(_canMove) WaveMove(); } private void WaveMove() { //Get the current x and y positions _xPos = _transform.position.x; _yPos = _transform.position.y; //Check which direction we need to calculate if (waveDirection == WaveMovementDirection.horizontal) { //Calculate horizontal motion _xMotion = _parentTransform != null ? _parentTransform.position.x : _transform.position.x; _xPos = _xMotion + Mathf.Cos(_waveNum) * maxWave; //Check if the object should move vertically if (moveObject) _yPos += moveSpeed * moveDirection.y; } else { //Calculate vertical motion _yMotion = _parentTransform != null ? _parentTransform.position.y : _transform.position.y; _yPos = _yMotion + Mathf.Cos(_waveNum) * maxWave; //Check if the object should move horizontally if (moveObject) _xPos += moveSpeed * moveDirection.x; } //Assign x and y positions to transform _movementVector.Set(_xPos, _yPos, 0); _transform.position = _movementVector; //Increment _waveNum to get the motion going _waveNum += 0.1f; } public void PlayWaveMovement(bool value) { if(!disableEventCall) _canMove = value; } }
SnakeMovement.cs
The following code is assigned to the “dragon_head” gameObject and is responsible for the motion of each body part in relation to the “dragon_head” gameObject. Basically, a transform reference for each body part is stored in the _bodyParts List array and in the FixedUpdate we check each body part against its previous body part to determine where we need the current body part in the loop to move in the next frame – I know it sounds confusing but it works. The code also checks to see which way each body part sprite should be facing (or rather flipped) using InverseTransformPoint.
using System.Collections; using System.Collections.Generic; using UnityEngine; public class SnakeMovement : MonoBehaviour { [Header("Child Objects")] [SerializeField] private List<Transform> _bodyParts = new List<Transform>(); [SerializeField] private float _minDistance = .25f; [SerializeField] private float _speed = 2f; [SerializeField] private float _bodyPartSpacing = 5f; //Interal vars private float _distance; private Transform _currBodyPart; private Transform _prevBodyPart; private Rigidbody2D _firstBodyPart; private bool _canMove = true; private Vector3 _newPosition; private float _time; private SpriteRenderer _sprite; private Transform _targetTransform; private Vector3 _relativePoint; private void Start() { if(_bodyParts.Count > 0) _firstBodyPart = _bodyParts[0].GetComponent<Rigidbody2D>(); } // Update is called once per frame void FixedUpdate() { if(_canMove) { for (int i = 1; i < _bodyParts.Count; i++) { //Get a reference the current and previous body part within the loop _currBodyPart = _bodyParts[i]; _prevBodyPart = _bodyParts[i - 1]; //Check the distance between the current and previous body parts _distance = Vector3.Distance(_prevBodyPart.position, _currBodyPart.position); //Calculate a new position _newPosition = _prevBodyPart.position; _newPosition.x = _bodyParts[i - 1].position.x + (_firstBodyPart.transform.localScale.x > 0 ? -_bodyPartSpacing : _bodyPartSpacing); _newPosition.y = _bodyParts[i - 1].position.y; //Calculate the timing for the slerp functions _time = Time.deltaTime * _distance / _minDistance * _speed; //Cap the time if (_time > 0.5f) _time = 0.5f; //Use slerp functions to apply a smooth motion _currBodyPart.position = Vector3.Slerp(_currBodyPart.position, _newPosition, _time); _currBodyPart.rotation = Quaternion.Slerp(_currBodyPart.rotation, _prevBodyPart.rotation, _time); //Lets flip the sprites based on prev body part position _sprite = _bodyParts[i].GetComponent<SpriteRenderer>(); _targetTransform = _bodyParts[i - 1].transform; _relativePoint = _bodyParts[i].transform.InverseTransformPoint(_targetTransform.position); if (_relativePoint.x > 0) _sprite.flipX = false; else _sprite.flipX = true; } } } public void StopSnakeMovement() { _canMove = false; } }
And that’s pretty much it – feel free to play around with the settings in each component to get the desired motion you want for your “snake” like motion – there are controls available in the SnakeMovement script for fine tuning the motion and spacing of each body part.
If you’d like to face off against my dragon feel free to try out Shadow of the Orient which is currently in early access.
Have a question? Feel free to post a comment a below.