-
-
Notifications
You must be signed in to change notification settings - Fork 45
Home
- Is the source project free to use?
- How do I prevent the snake from turning in on itself?
- How can I modify the speed of the snake without changing the fixed timestep?
- How do I prevent food from spawning on the snake?
- How do I make the snake move through walls to the opposite side?
- How can I implement arcade controls?
- How do I rotate the snake segments to match their direction?
- How do I write the "β " symbol in the code?
Yes, you are free to use the project for any purpose. However, keep in mind that copyright and/or trademark laws can still apply to the original material since many of the games in my tutorials were not originally authored by me. I open source my own code for the community to learn from, but the project is intended for educational purposes only.
If the snake is moving left or right, then it should only be able to turn up or down, and vice versa. Otherwise, the snake is able to turn in on itself and the player instantly loses. This is a problem we forgot to account for in the original tutorial. There is also a similar but different problem where the snake can turn in on itself due to the timing difference between Update
(where inputs are read) and FixedUpdate
(where the snake movement occurs). We will fix both issues with the same solution.
First, in the Snake script we need to add a new variable to the class to store the user input.
private Vector2 input;
Instead of assigning the direction
, we will assign input
inside the Update
function.
private void Update()
{
if (Input.GetKeyDown(KeyCode.W) || Input.GetKeyDown(KeyCode.UpArrow)) {
input = Vector2.up;
} else if (Input.GetKeyDown(KeyCode.S) || Input.GetKeyDown(KeyCode.DownArrow)) {
input = Vector2.down;
} else if (Input.GetKeyDown(KeyCode.D) || Input.GetKeyDown(KeyCode.RightArrow)) {
input = Vector2.right;
} else if (Input.GetKeyDown(KeyCode.A) || Input.GetKeyDown(KeyCode.LeftArrow)) {
input = Vector2.left;
}
}
We also need to add a couple if statements to only assign the input based on the current direction of the snake. If the snake is moving up/down, then we only look for left/right inputs, and vice versa.
private void Update()
{
// Only allow turning up or down while moving in the x-axis
if (direction.x != 0f)
{
if (Input.GetKeyDown(KeyCode.W) || Input.GetKeyDown(KeyCode.UpArrow)) {
input = Vector2.up;
} else if (Input.GetKeyDown(KeyCode.S) || Input.GetKeyDown(KeyCode.DownArrow)) {
input = Vector2.down;
}
}
// Only allow turning left or right while moving in the y-axis
else if (direction.y != 0f)
{
if (Input.GetKeyDown(KeyCode.D) || Input.GetKeyDown(KeyCode.RightArrow)) {
input = Vector2.right;
} else if (Input.GetKeyDown(KeyCode.A) || Input.GetKeyDown(KeyCode.LeftArrow)) {
input = Vector2.left;
}
}
}
Lastly, we assign the new direction to the input in FixedUpdate
. This prevents the timing issue. The rest of the movement code remains the same.
private void FixedUpdate()
{
if (input != Vector2.zero) {
direction = input;
}
//...
}
We use fixed timestep in the tutorial to control how the fast the snake moves while maintaining perfectly aligned grid positions. This is an easy solution, but it doesn't provide for a lot of flexibility. Fixed timestep affects all of the objects in your game, not just the snake. Let's look at how we can implement this differently.
The first thing we want to do is add new variables to our Snake script to customize its speed. The speed
variable represents the base speed of the snake, and the speedMultiplier
is an optional value that can be used to easily apply modifiers.
public class Snake : MonoBehaviour
{
//...
public float speed = 20f;
public float speedMultiplier = 1f;
}
We also need to add a variable to indicate when the next movement update should take place.
public class Snake : MonoBehaviour
{
//...
private float nextUpdate;
}
Finally, we update our FixedUpdate
function to only execute the movement code if the current time exceeds the next update, otherwise we call return
to break out of the function before executing the logic. At the very end of the function, we set the nextUpdate
based on how quickly our snake is moving. Everything else in between is exactly the same as before.
private void FixedUpdate()
{
// Wait until the next update before proceeding
if (Time.time < nextUpdate) {
return;
}
// Set each segment's position to be the same as the one it follows. We
// must do this in reverse order so the position is set to the previous
// position, otherwise they will all be stacked on top of each other.
for (int i = segments.Count - 1; i > 0; i--) {
segments[i].position = segments[i - 1].position;
}
// Move the snake in the direction it is facing
// Round the values to ensure it aligns to the grid
float x = Mathf.Round(transform.position.x) + direction.x;
float y = Mathf.Round(transform.position.y) + direction.y;
transform.position = new Vector2(x, y);
nextUpdate = Time.time + (1f / (speed * speedMultiplier));
}
To prevent food from spawning on the snake, we need to check if the position of the food is the same as any of the snake segments. If it is the same, we will update the food position to a new position.
First, we need to add a helper function to the Snake.cs
script to check if a set of coordinates are occupied by the snake. The following code loops through each snake segment and checks if the position is equal to the provided coordinates. If they are equal, it returns true, otherwise it returns false.
public bool Occupies(float x, float y)
{
foreach (Transform segment in segments)
{
if (segment.position.x == x && segment.position.y == y) {
return true;
}
}
return false;
}
Now we can use this helper function in the Food.cs
script, but we need a reference to the snake so we can call the function. We'll add a new variable to the class and assign the value in Awake
.
private Snake snake;
private void Awake()
{
snake = FindObjectOfType<Snake>();
}
Finally, we can call the helper function to check if the food coordinates are occupied by the snake. We will advance the food position to the next row/column until the position is not occupied by the snake.
public void RandomizePosition()
{
Bounds bounds = gridArea.bounds;
// Pick a random position inside the bounds
float x = Random.Range(bounds.min.x, bounds.max.x);
float y = Random.Range(bounds.min.y, bounds.max.y);
// Round the values to ensure it aligns with the grid
x = Mathf.Round(x);
y = Mathf.Round(y);
// Prevent food from spawning on the snake
while (snake.Occupies(x, y))
{
x++;
if (x > bounds.max.x)
{
x = bounds.min.x;
y++;
if (y > bounds.max.y) {
y = bounds.min.y;
}
}
}
// Assign the final position
transform.position = new Vector2(x, y);
}
In many implementations of the game the snake is able to move through walls to go to the opposite side. First, let's change the tag of each wall from "Obstacle" to "Wall". This allows us to distinguish them from other objects.
Instead of calling the ResetState
function when the snake collides with a wall, we update its position to move to the opposite wall. This code takes place in the OnTriggerEnter2D
when we detect the snake has collided with a "Wall".
private void OnTriggerEnter2D(Collider2D other)
{
if (other.gameObject.CompareTag("Food"))
{
Grow();
}
else if (other.gameObject.CompareTag("Obstacle"))
{
ResetState();
}
else if (other.gameObject.CompareTag("Wall"))
{
Vector3 position = transform.position;
if (direction.x != 0f) {
position.x = -other.transform.position.x + direction.x;
} else if (direction.y != 0f) {
position.y = -other.transform.position.y + direction.y;
}
transform.position = position;
}
}
Source code: https://github.com/zigurous/unity-snake-tutorial/blob/arcade-controls/Assets/Scripts/Snake.cs#L21-L47
With arcade controls the snake will only move when the player presses an input rather than move automatically based on the current direction.
Since input is checked in the Update
function, we need to move our movement code to that same function. We'll take the code from FixedUpdate
and move it to the bottom of Update
.
private void Update()
{
//...
// Set each segment's position to be the same as the one it follows. We
// must do this in reverse order so the position is set to the previous
// position, otherwise they will all be stacked on top of each other.
for (int i = segments.Count - 1; i > 0; i--) {
segments[i].position = segments[i - 1].position;
}
// Move the snake in the direction it is facing
// Round the values to ensure it aligns to the grid
int x = Mathf.RoundToInt(transform.position.x) + direction.x;
int y = Mathf.RoundToInt(transform.position.y) + direction.y;
transform.position = new Vector2(x, y);
}
Then we just need to make some minor modifications to how we handle input. Mainly we need to exit out of the function and not execute the code above if the player does not press any input. We'll do this by calling return
if none of the inputs are pressed.
private void Update()
{
if (Input.GetKeyDown(KeyCode.W) || Input.GetKeyDown(KeyCode.UpArrow)) {
direction = Vector2Int.up;
} else if (Input.GetKeyDown(KeyCode.S) || Input.GetKeyDown(KeyCode.DownArrow)) {
direction = Vector2Int.down;
} else if (Input.GetKeyDown(KeyCode.D) || Input.GetKeyDown(KeyCode.RightArrow)) {
direction = Vector2Int.right;
} else if (Input.GetKeyDown(KeyCode.A) || Input.GetKeyDown(KeyCode.LeftArrow)) {
direction = Vector2Int.left;
} else {
return;
}
//...
}
Altogether, the Update
function looks like this:
private void Update()
{
if (Input.GetKeyDown(KeyCode.W) || Input.GetKeyDown(KeyCode.UpArrow)) {
direction = Vector2Int.up;
} else if (Input.GetKeyDown(KeyCode.S) || Input.GetKeyDown(KeyCode.DownArrow)) {
direction = Vector2Int.down;
} else if (Input.GetKeyDown(KeyCode.D) || Input.GetKeyDown(KeyCode.RightArrow)) {
direction = Vector2Int.right;
} else if (Input.GetKeyDown(KeyCode.A) || Input.GetKeyDown(KeyCode.LeftArrow)) {
direction = Vector2Int.left;
} else {
return;
}
// Set each segment's position to be the same as the one it follows. We
// must do this in reverse order so the position is set to the previous
// position, otherwise they will all be stacked on top of each other.
for (int i = segments.Count - 1; i > 0; i--) {
segments[i].position = segments[i - 1].position;
}
// Move the snake in the direction it is facing
// Round the values to ensure it aligns to the grid
int x = Mathf.RoundToInt(transform.position.x) + direction.x;
int y = Mathf.RoundToInt(transform.position.y) + direction.y;
transform.position = new Vector2(x, y);
}
There is quite a bit of complexity to this problem, so view the source code linked above for the full solution. In order to solve this problem, each individual segment in the snake needs to keep track of its direction so it can be rotated based on that direction. A new script has been created called SnakeSegment
which is added to each individual segment of the snake. This script stores the direction of the segment and calculates the rotation of the segment based on any change in direction.
The script also updates the sprite for the segment depending on if the segment is the head piece, tail piece, body piece, or corner piece. This is determined based on the index of the segment in the list, as well as if the segment is changing direction (turning) or not.
- Source code: https://github.com/zigurous/unity-snake-tutorial/tree/sprite-orientation
- Full changes: https://github.com/zigurous/unity-snake-tutorial/compare/main...sprite-orientation
In the video I had font ligatures turned on in my editor which displays certain character combinations differently. This is something I've turned off for future videos to prevent confusion for those who aren't familiar with it. The β
symbol is actually written as !=
, which means "not equal".