Implement the snake movement in unity

I am developing a bullet-hell game and struggling to implement the snake movement logic. I try to record the head's positions and update bodies with these positions.

using Saltyfish.Util;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace Saltyfish.Logic
{
public class SnakeMovement : MonoBehaviour
{

[SerializeField]
private SnakePath m_SnakePath = new SnakePath(10);

[SerializeField]
private List<Unit> m_Bodies = new List<Unit>();

public List<Unit> Bodies => m_Bodies;

[SerializeField]
[Range(0, 5)]
private float m_BodySpace;

// Update is called once per frame
void Update()
{
OnUpdate();
}

protected virtual void OnUpdate()
{
UpdatePath();
UpdateBodies();
}

protected void UpdateMovement(Vector2 direction)
{
return;
Vector2 curPos = transform.position;
var nextPos = curPos + direction * Head.GetSpeed() * Time.deltaTime;
transform.position = new Vector3(nextPos.x, nextPos.y, transform.position.z);
}

private void UpdatePath()
{
if (m_SnakePath.Count == 0)
{
{
}
}
else
{
if(distance >= m_BodySpace)
{
}
}
}

public void SetBodyCapacity(int capacity)
{
m_SnakePath?.Resize(capacity);
}

private void UpdateBodies()
{
if (m_Bodies.Count <= 1)
return;
return;
for (int i = 1;i < m_Bodies.Count; ++i)
{
continue;
var prevPoint = m_SnakePath.GetPointOrTail(i);
var nextPoint = m_SnakePath.GetPointOrTail(i - 1);
if (!nextPoint.HasValue || !prevPoint.HasValue)
continue;
var nextPos = nextPoint.Value;
var prevPos = prevPoint.Value;
}
}

public void InitBodies(IList<Unit> units)
{
m_Bodies.Clear();
if(units.Count > m_SnakePath.MaxCapacity)
{
Debug.LogWarning("body is too long");
}
for(int i = 0;i < units.Count && i < m_SnakePath.MaxCapacity; i++)
{
}
}

public void PushBody(Unit unit)
{
if(m_Bodies.Count >= m_SnakePath.MaxCapacity)
{
Debug.LogWarning("not enough capacity for new body");
return;
}
if(m_Bodies.Contains(unit))
{
return;
}
}

public void InsertBody(Unit unit, int pos = 0)
{
if (pos < 0 || pos >= m_Bodies.Count)
return;
if (m_Bodies.Count >= m_SnakePath.MaxCapacity)
{
Debug.LogWarning("not enough capacity for new body");
return;
}
m_Bodies.Insert(pos, unit);
}

public Unit GetBody(int pos)
{
var unit = m_Bodies.SafeGet(pos);
return unit;
}

public bool RemoveBody(Unit unit)
{
return m_Bodies.Remove(unit);
}

public void RemoveAt(int pos)
{
m_Bodies.RemoveAt(pos);
}

#if UNITY_EDITOR

private void OnDrawGizmos()
{
for(int i = 0;i < m_SnakePath.Count; i++)
{
Gizmos.DrawSphere(m_SnakePath.GetPoint(i).Value, 1);
}
}
#endif
}
}

using Saltyfish.Util;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;

namespace Saltyfish.Logic
{
[Serializable]
class SnakePath
{
[SerializeField]
private List<Vector3> m_PathPoints = new List<Vector3>();

public int MaxCapacity { get;  private set; }

public int Count => m_PathPoints.Count;

{
get
{
if (m_PathPoints.Count == 0)
return null;
return m_PathPoints[0];
}
}

public Vector3? Tail
{
get
{
if (m_PathPoints.Count == 0)
return null;
return m_PathPoints[m_PathPoints.Count - 1];
}
}

public SnakePath(int capacity)
{
MaxCapacity = capacity;
}

public void Resize(int capacity)
{
if (capacity <= 0)
return;
if (m_PathPoints.Count > capacity)
m_PathPoints.RemoveRange(capacity, m_PathPoints.Count - capacity);
MaxCapacity = capacity;
}

{
m_PathPoints.Insert(0, point);
if (m_PathPoints.Count >= MaxCapacity)
RemoveAt(m_PathPoints.Count - 1);
}

public void RemoveAt(int idx)
{
m_PathPoints.RemoveAt(idx);
}

public Vector3? GetPoint(int idx)
{
if (idx < 0 || idx >= m_PathPoints.Count)
return null;
return m_PathPoints[idx];
}

public Vector3? GetPointOrTail(int idx)
{
if (m_PathPoints.Count == 0)
return null;
var point = GetPoint(idx);
if (point == null)
point = m_PathPoints[m_PathPoints.Count - 1];
return point;
}
}
}



But the current movement is less smooth if the gap(bodyspace) becomes larger.

So I searched a lot for a better implementation and I found these below.

Snake tail follow

They are very helpful, but I think there are still some defects in these solutions.

Defect 1. All of the create-body 's scripts in these solutions used a delay time to keep a fixed gap between bodies. What if the snake's speed is various, which leads to different gaps between body parts?

Defect 2. How could I insert a body to the snake or swap a body with another and makes the snake apply the change automatically? ( all of these solutions are simply appending a body to the tail, which will also cause same problem in defect 1 if the snake speed has changed).

How could I implement a smooth snake movement with a dynamic body management?

I found a solution that uses backtracking.

It records the snake route every frame, and the bodies trace the route back to find their property position.

using Saltyfish.Util;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace Saltyfish.Logic
{
public class SnakeMovement : MonoBehaviour
{
[SerializeField]

private List<SnakePoint> m_SnakePath = new List<SnakePoint>();

[SerializeField]
private List<Unit> m_Bodies = new List<Unit>();

public List<Unit> Bodies => m_Bodies;

[SerializeField]
[Range(0.1f, 5)]
private float m_BodySpace;

private bool m_Moved;

// Update is called once per frame
void Update()
{
OnUpdate();
}

protected virtual void OnUpdate()
{
UpdateBodies();
}

protected void UpdateMovement(Vector2 direction)
{
return;
Vector2 curPos = transform.position;
var nextPos = curPos + direction * Head.GetSpeed() * Time.deltaTime;
transform.position = new Vector3(nextPos.x, nextPos.y, transform.position.z);
// Optimization 1 : Only update path when moved
if(direction != Vector2.zero)
{
UpdatePath();
}
// Optimization 1
}

private void UpdatePath()
{
return;
if (m_SnakePath.Count > 0)
{
var lastPoint = m_SnakePath[m_SnakePath.Count - 1];
}
}
private void UpdateBodies()
{
if (m_Bodies.Count <= 1)
return;
for (int i = 1; i < m_Bodies.Count; ++i)
{
continue;
float remainDistance = Mathf.Clamp(m_BodySpace, 0.1f, 5) * i;
for (int j = m_SnakePath.Count - 1; j > 0; j--)
{
{
float LerpProgress = 0;
{
}
m_Bodies[i].transform.position = Vector3.Lerp(
m_SnakePath[j].Position,
m_SnakePath[j - 1].Position,
LerpProgress);
// Optimization 2 : remove the points before the waypoint that last body has reached
if (i == m_Bodies.Count - 1)
{
m_SnakePath.RemoveRange(0, j - 1);
}
// Optimization 2
break;
}
}
}
}

public void InitBodies(IList<Unit> units)
{
m_Bodies.Clear();
for (int i = 0; i < units.Count; i++)
{
}
}

public void PushBody(Unit unit)
{
if (m_Bodies.Contains(unit))
{
return;
}
}

public void InsertBody(Unit unit, int pos = 0)
{
if (pos < 0 || pos >= m_Bodies.Count)
return;
m_Bodies.Insert(pos, unit);
}

public Unit GetBody(int pos)
{
var unit = m_Bodies.SafeGet(pos);
return unit;
}

public bool RemoveBody(Unit unit)
{
return m_Bodies.Remove(unit);
}

public void RemoveAt(int pos)
{
m_Bodies.RemoveAt(pos);
}

private void OnGUI()
{
GUIStyle textStyle = new GUIStyle();
textStyle.fontSize = 35;
GUI.Label(new Rect(25, 25, 200, 100),"BodySpace", textStyle);
m_BodySpace = GUI.HorizontalSlider(new Rect(25, 100, 200, 80), m_BodySpace, 0.1F, 5);
}

}
}

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;

namespace Saltyfish.Logic
{
[Serializable]
public class SnakePoint
{
public Vector3 Position;

public SnakePoint(Vector3 pos)
{
Position = pos;
}
}
}



Although the route point list grows endlessly, the memory cost is acceptable because a single game lasts only 5 minutes. And I can clear the list before the next game begins.

EDIT:

1. The list does not need to be updated if the movement is stopped.
2. The points before the last body point can be removed as Zibelas suggested.
• Cant you remove the point as well once the last body part reached the waypoint? May 27 at 10:32
• @Zibelas Thx for your inspiration! I have changed the code so that it has a better space complexity. May 27 at 11:03
• Furthermore, I can add a object pool for the temporarily removed snake points and reuse them. May 27 at 11:10