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;
public Unit Head => m_Bodies.SafeGet(0);
// Update is called once per frame
void Update()
{
OnUpdate();
}
protected virtual void OnUpdate()
{
UpdatePath();
UpdateBodies();
}
protected void UpdateMovement(Vector2 direction)
{
if (Head == null)
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)
{
if(Head != null)
{
m_SnakePath.AddPoint(Head.transform.position);
}
}
else
{
var tailPoint = m_SnakePath.Head.Value;
var distance = Vector2.Distance(Head.transform.position, tailPoint);
if(distance >= m_BodySpace)
{
m_SnakePath.AddPoint(Head.transform.position);
}
}
}
public void SetBodyCapacity(int capacity)
{
m_SnakePath?.Resize(capacity);
}
private void UpdateBodies()
{
if (m_Bodies.Count <= 1)
return;
var headPoint = m_SnakePath.GetPoint(0);
if (!headPoint.HasValue)
return;
float headMoveDistance = (Head.transform.position - headPoint.Value).magnitude;
for (int i = 1;i < m_Bodies.Count; ++i)
{
if (m_Bodies[i].IsDead)
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;
m_Bodies[i].transform.position = Vector3.Lerp(prevPos, nextPos, headMoveDistance);
}
}
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++)
{
m_Bodies.Add(units[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;
}
m_Bodies.Add(unit);
}
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 System.Threading.Tasks;
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;
public Vector3? Head
{
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;
}
public void AddPoint(Vector3 point)
{
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.
Snakemovement tutorial on youtube
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?