4
\$\begingroup\$

I've made a simple snake game in Unity and it has 3 different types of foods.

Me dying

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;
    }
}
\$\endgroup\$
  • \$\begingroup\$ Do you happen to have a repository on GitHub with all your games? \$\endgroup\$ – t3chb0t Dec 26 '16 at 15:11
  • \$\begingroup\$ No I actually don't have one perhaps I should make one.. I will notify you somehow if I ever make one ^^ \$\endgroup\$ – Denis Dec 26 '16 at 15:13
4
\$\begingroup\$
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;
         }
    }
}

Not sure if I'm missing something, but I don't see the point of calling SetDirection and iterating through an array four times to set the direction - you only expect one of these directions to be set, right?

Also it might be more efficient to do this check if (snake.Count == 1 || moveDirection != oppositeDirection) before if (triggers.Any(Input.GetKeyDown)) - technically they could all be in the same if statement, but better to shortcut with a couple of simple comparisons before going through an iterator. (Unless Vector3 has overloaded the operators to do some complex logic...)

Overall I think it would be more efficient to create a dictionary with the various keys of interest, look up Input.GetKeyDown (it returns a KeyCode, right?) in the dictionary, and if found use the corresponding value. Although since you only want to move in the direction if it's not the "opposite" direction, you could either have another dictionary for storing opposites or create a class, e.g.

class Direction
{
    public Vector3 Desired { get; }
    public Vector3 Opposite { get; }
}

Then setup a dictionary, e.g.

private readonly Dictionary<KeyCode, Direction> keycodeToDirection = new Dictionary<KeyCode, Direction>()
{
    { KeyCode.LeftArrow, new Direction() { Desired = Vector3.left, Opposite = Vector3.right } },
    { KeyCode.A, new Direction() { Desired = Vector3.left, Opposite = Vector3.right } }
    // all the rest
    // edit: Just realised this wouldn't compile due to not being able to set the property values here, but that's not the point of the example... you get the idea.
};

Then:

private void Update()
{
    if (Input.anyKeyDown)
    {
        var direction = GetDirectionForKeyCode(Input.GetKeyDown);
        if (direction != null)
            SetDirection(direction);
    }
}

private Direction GetDirectionForKeyCode(KeyCode keyCode)
{
    Direction d;
    if (keycodeToDirection.TryGetValue(keyCode, out d)
    {
        if (snake.Count == 1 || moveDirection != d.Opposite)
            return d;
    }
    return null;
}

private void SetDirection(Direction direction)
{
    moveDirection = direction.Desired;
    IsMoving = true;
}

private void OnTriggerEnter2D(Collider2D other)
{
    if (other.tag == "Wall")
    {
        Loss();
    }
    if (other.tag == "SnakePart" && invulnerabilityTime <= Time.time)
    {
        Loss();
    }
}

Could be written with a single if statement:

private void OnTriggerEnter2D(Collider2D other)
{
    if (other.tag == "Wall" || (other.tag == "SnakePart" && invulnerabilityTime <= Time.time))
    {
        Loss();
    }
}

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.