I've made a simple snake game in Unity and it has 3 different types of foods.
Note - 5 blocks = 1 point
This is a screenshot from the game here I'm colliding with myself and the game is restarted after that. As you can see the snake has a highlighted head (the light green block) and it consists of quite a lot smaller blocks and there are those outlines on the body of the snake they are created on each 10 blocks you get;
The snake is controlled by the SnakeHead
script
public class SnakeHead : MonoBehaviour
{
public bool IsMoving { get; set; }
private const float Speed = .2f;
public ObjectPooler snakePartsObjectPooler;
public Text ScoreText;
private Vector3 moveSize;
private Vector3 moveDirection;
private float invulnerabilityTime;
private readonly List<GameObject> snake = new List<GameObject>();
private void Start()
{
moveSize = GetComponent<BoxCollider2D>().bounds.size;
snake.Add(gameObject);
StartCoroutine(Move());
}
private void Update()
{
if (Input.anyKeyDown)
{
SetDirection(Vector3.left, Vector3.right, KeyCode.LeftArrow, KeyCode.A);
SetDirection(Vector3.right, Vector3.left, KeyCode.RightArrow, KeyCode.D);
SetDirection(Vector3.up, Vector3.down, KeyCode.UpArrow, KeyCode.W);
SetDirection(Vector3.down, Vector3.up, KeyCode.DownArrow, KeyCode.S);
}
}
private void SetDirection(Vector3 direction, Vector3 oppositeDirection, params KeyCode[] triggers)
{
if (triggers.Any(Input.GetKeyDown))
{
if (snake.Count == 1 || moveDirection != oppositeDirection)
{
moveDirection = direction;
IsMoving = true;
}
}
}
private IEnumerator Move()
{
do
{
for (int i = snake.Count - 1; i > 0; i--)
{
snake[i].transform.position = snake[i - 1].transform.position;
}
transform.position = transform.position +
new Vector3((moveSize.x*moveDirection.x)*Speed, (moveSize.y*moveDirection.y)*Speed);
yield return null;
} while (gameObject.activeInHierarchy);
}
private void OnTriggerEnter2D(Collider2D other)
{
if (other.tag == "Wall")
{
Loss();
}
if (other.tag == "SnakePart" && invulnerabilityTime <= Time.time)
{
Loss();
}
}
private void Loss()
{
SceneManager.LoadScene(SceneManager.GetActiveScene().name);
}
public void Eat(Food food)
{
Vector3 direction = new Vector3(moveDirection.x*-1, moveDirection.y*-1);
int orderInLayer = snake.Last().GetComponent<SpriteRenderer>().sortingOrder - 1;
for (int i = 0; i < food.Points; i++)
{
invulnerabilityTime = Time.time + .2f;
GameObject snakePart = snakePartsObjectPooler.GetPooledObject();
snakePart.SetActive(true);
if (snake.Count <= 10)
{
snakePart.GetComponent<BoxCollider2D>().enabled = false;
}
if (snake.Count%10 != 0)
{
snakePart.GetComponent<SpriteRenderer>().sortingOrder = orderInLayer;
}
else
{
snakePart.GetComponent<SpriteRenderer>().sortingOrder++;
}
snake.Add(snakePart);
snake[snake.Count - 1].transform.position = snake[snake.Count - 2].transform.position;
}
ScoreText.text = @"Score : " + (snake.Count - 1)/5;
}
}
An interesting field here is private float invulnerabilityTime;
. It helps preventing the snake from crashing in itself when a food is eaten, there is a possibility that the blocks will spawn in front of the snake depending on the direction it's moving, even tho as a user you wont be able to notice that since it's a matter of milliseconds the program will "see" it and will cause invalid collision. The invulnerability is being set to 200ms (probably can down that a bit) whenever a food is eaten to prevent this effect.
Next we have the Food
class
[RequireComponent(typeof(BoxCollider2D))]
public class Food : MonoBehaviour
{
public int Points;
public float Duration;
private float timeToRemove;
private void Start()
{
Initialize();
}
public void Initialize()
{
timeToRemove = Time.time + Duration;
}
private void Update()
{
if (Time.time >= timeToRemove)
{
gameObject.SetActive(false);
}
}
private void OnTriggerEnter2D(Collider2D other)
{
if (other.name == "Snake")
{
other.GetComponent<SnakeHead>().Eat(this);
gameObject.SetActive(false);
}
}
}
This class is attached to a GameObject which also have some image on it and by changing those 2 values a new food type is created.
And of course the FoodSpawner
class which creates the food during the game
public class FoodSpawner : MonoBehaviour
{
private enum Axis
{
X,
Y
}
public ObjectPooler[] ObjectPoolers;
public float Frequency;
private float nextSpawn;
public GameObject Snake;
public bool Enabled { get; set; }
private static int maxStepsX;
private static int maxStepsY;
private static readonly System.Random rnd = new System.Random();
private GameObject background;
private Vector2 snakeSize;
private void Awake()
{
snakeSize = Snake.GetComponent<Collider2D>().bounds.size;
background = GameObject.Find("Background");
maxStepsX = GetSteps(Axis.X);
maxStepsY = GetSteps(Axis.Y);
}
private void Update()
{
if (Snake.GetComponent<SnakeHead>().IsMoving && Time.time >= nextSpawn)
{
GameObject food = ObjectPoolers[rnd.Next(0, ObjectPoolers.Length)].GetPooledObject();
food.GetComponent<Food>().Initialize();
food.SetActive(true);
int side = new[] {1, -1}[rnd.Next(0, 2)];
food.transform.position = GetPositionBySteps(rnd.Next(0, maxStepsX - 2) * side, rnd.Next(0, maxStepsY - 2) * side);
nextSpawn = Time.time + Frequency;
}
}
private Vector3 GetPositionBySteps(int xSteps, int ySteps)
{
return new Vector3(snakeSize.x*xSteps, snakeSize.y*ySteps);
}
private int GetSteps(Axis axis)
{
int currentSteps = 0;
Vector3 currentPosition = Vector3.zero;
while (background.GetComponent<SpriteRenderer>().bounds.Contains(currentPosition))
{
currentPosition = axis == Axis.X
? new Vector3(currentPosition.x + snakeSize.x, currentPosition.y, 0)
: new Vector3(currentPosition.x, currentPosition.y + snakeSize.y, 0);
currentSteps++;
}
return currentSteps;
}
}
The main role here are playing the maxSteps
fields
private static int maxStepsX;
private static int maxStepsY;
They are being calculated only once on Awake and are used to determine how much "steps" can be made in each axis, where a step is considered the size of the snake head block, later a random variable is picked between those values to determine where to spawn food.
rnd.Next(0, maxStepsX - 2)
the magic number here 2, is just to prevent food being spawned really close to the wall.
I also have this ObjectPooler
class
public class ObjectPooler : MonoBehaviour
{
public GameObject PooledObject;
public int PooledAmount;
public bool CanGrow;
private readonly List<GameObject> pooledObjects = new List<GameObject>();
private void Awake()
{
for (int i = 0; i < PooledAmount; i++)
{
GameObject obj = Instantiate(PooledObject);
obj.SetActive(false);
pooledObjects.Add(obj);
}
}
public GameObject GetPooledObject()
{
for (int i = 0; i < pooledObjects.Count; i++)
{
if (!pooledObjects[i].activeInHierarchy)
{
return pooledObjects[i];
}
}
if (CanGrow)
{
GameObject obj = Instantiate(PooledObject);
pooledObjects.Add(obj);
return obj;
}
return null;
}
}