SnakeTweaked.cs
Assets > Scripts > SnakeTweaked.cs
Assets > Scripts > SnakeTweaked.cs
using System.Collections.Generic;
using System.Collections;
using UnityEngine.SceneManagement;
using UnityEngine;
using Random = UnityEngine.Random;
using UnityEngine.Events;
using UnityEditor;
public class SnakeTweaked : MonoBehaviour
{
[SerializeField] GameObject snakePrefab;
[SerializeField] GameObject snakeHeadClosed;
[SerializeField] GameObject snakeHeadOpened;
[SerializeField] Transform snakeHead;
[SerializeField] List<Transform> snakeBody = new List<Transform>();
[SerializeField] Transform snakeTail;
bool hasSnakeGrown = false;
[SerializeField] Vector3 moveRightOffset;
[SerializeField] Vector3 moveLeftOffset;
[SerializeField] Vector3 moveUpOffset;
[SerializeField] Vector3 moveDownOffset;
[SerializeField] Camera MainCamera;
[SerializeField] AnimationClip CameraShake;
bool isSnakeDead;
public UnityEvent snakeDeath;
public int Score = 0;
public int FirstDigitScore = 0;
public int SecondDigitScore = 0;
public int ThirdDigitScore = 0;
public int FourthDigitScore = 0;
Vector3 inputDirection;
void Start()
{
inputDirection = moveRightOffset;
InvokeRepeating(nameof(MoveSnakeEveryHalfSecond), 0.5f, 0.5f);
Cursor.visible = true;
}
void Update()
{
if (isSnakeDead == false)
{
InputDetection();
}
else if (isSnakeDead == true)
{
CancelInvoke();
snakeDeath.Invoke();
}
}
void OnTriggerEnter2D(Collider2D other)
{
if (other.CompareTag("Apple"))
{
Debug.Log ("Trying to eat apple!");
Vector3 newApplePos = new Vector3();
newApplePos = new Vector3(Random.Range(-7.2f, 7.3f), Random.Range(-2, 3), 0);
other.transform.position = newApplePos;
GameObject newSnakeBodyPart = Instantiate(snakePrefab, snakeBody[snakeBody.Count - 1].transform.position, Quaternion.identity);
hasSnakeGrown = true;
snakeBody.Add(newSnakeBodyPart.transform);
AddScore();
}
if (other.CompareTag("AppleProximity"))
{
Debug.Log ("Within proximity of apple!");
}
if (other.CompareTag("SnakeBodyDZ"))
{
isSnakeDead = true;
}
}
IEnumerator SnakeBodyDZHit()
{
MainCamera.GetComponent<Animator>().enabled = true;
yield return new WaitForSeconds(1);
RestartGame();
}
public void Death()
{
StartCoroutine(SnakeBodyDZHit());
}
public void QuitGame()
{
#if UNITY_EDITOR
{
EditorApplication.isPlaying = false;
}
#else
{
Application.Quit();
}
#endif
}
void AddScore()
{
Score++;
FourthDigitScore++;
Debug.Log("Added a point to the fourth digit");
if (Score == 10 || Score == 20 || Score == 30 || Score == 40 || Score == 50 || Score == 60 || Score == 70 || Score == 80 || Score == 90)
{
ThirdDigitScore++;
FourthDigitScore = 0;
Debug.Log("Added a point to the third digit");
}
else if (Score == 100 || Score == 200 || Score == 300 || Score == 400 || Score == 500 || Score == 600 || Score == 700 || Score == 800 || Score == 900)
{
SecondDigitScore++;
ThirdDigitScore = 0;
FourthDigitScore = 0;
Debug.Log("Added a point to the second digit");
}
else if (Score == 1000 || Score == 2000 || Score == 3000 || Score == 4000 || Score == 5000 || Score == 6000 || Score == 7000 || Score == 8000 || Score == 9000)
{
FirstDigitScore++;
SecondDigitScore = 0;
ThirdDigitScore = 0;
FourthDigitScore = 0;
Debug.Log("Added a point to the first digit");
}
Debug.Log("Score = " + Score);
}
void MoveSnakeEveryHalfSecond()
{
if (hasSnakeGrown)
{
hasSnakeGrown = false;
}
else
{
snakeTail.position = snakeBody[snakeBody.Count-1].position;
}
for (int i = snakeBody.Count-1; i > 0; i--)
{
snakeBody[i].position = snakeBody[i - 1].position;
}
snakeBody[0].position = snakeHead.position;
snakeHead.position += inputDirection;
snakeHead.position = CheckBoundary(snakeHead.position);
}
void InputDetection()
{
if (Input.GetKeyDown(KeyCode.UpArrow))
{
inputDirection = moveUpOffset;
}
else if (Input.GetKeyDown(KeyCode.DownArrow))
{
inputDirection = moveDownOffset;
}
else if (Input.GetKeyDown(KeyCode.RightArrow))
{
inputDirection = moveRightOffset;
}
else if (Input.GetKeyDown(KeyCode.LeftArrow))
{
inputDirection = moveLeftOffset;
}
}
void RestartGame()
{
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
}
Vector3 CheckBoundary(Vector3 pos)
{
if (pos.x > 7.5f)
return new Vector3(-7.2f, pos.y, 0);
if (pos.x < -7.2f)
return new Vector3(7.5f, pos.y, 0);
if (pos.y > 3)
return new Vector3(pos.x, -5, 0);
if (pos.y < -5)
return new Vector3(pos.x, 3, 0);
return pos;
}
}
This code will be explained in sections, which each have a different colour to separate them from each other. Comments in the script have been removed from this excerpt, but will be included in the Google Doc containing the script, linked at the bottom of the page.
SECTION 1: Setting up
This part of the script is for setting up the namespaces and variables to be used in the script.
using System.Collections.Generic
using System.Collections
using UnityEngine.SceneManagement
using UnityEngine
using Random = UnityEngine.Random
using UnityEngine.Events
using UnityEditor
Because this script is longer and uses more functions, it's expected that the script will use more namespaces. For this script, seven different namespaces are used, including System.Collections.Generic and System.Collections for using the IEnumerator functions (timers and such), UnityEngine.SceneManagement for restarting the level, UnityEngine for pretty much everything else Unity needs that standard C# doesn't provide, UnityEngine.Random for the Random algorithm, UnityEngine.Events to use the Event system and UnityEditor to allow checking for the Editor and exiting to the Editor.
[SerializeField] GameObject snakePrefab
[SerializeField] GameObject snakeHeadClosed
[SerializeField] GameObject snakeHeadOpened
[SerializeField] Transform snakeHead
[SerializeField] List<Transform> snakeBody = new List<Transform>()
[SerializeField] Transform snakeTail
These variables are mainly for the snake GameObjects. The snakePrefab GameObject variable is for the entire snake, the snakeHeadClosed and snakeHeadClosed variables are unused variables that were supposed to be for the custom snake sprites (which had a closed and opened mouth), the snakeHead and snakeTail transform is to get the position, rotation and scale of the GameObject, and the snakeBody list creates a list for Transform variables. They also have the SerializeField attribute to show the variables in the editor. All of these variables are for controlling the snake.
[SerializeField] Vector3 moveRightOffset
[SerializeField] Vector3 moveLeftOffset
[SerializeField] Vector3 moveUpOffset
[SerializeField] Vector3 moveDownOffset
Vector3 inputDirection
These variables are responsible for assisting in the snake's turn function. The values of these variables are set in the inspector, hence the SerializeField attribute, and the values determine how far the snake moves in each increment. I've set them to 0.8 to match the size of the snake's body so it moves at the same distance as its own body size.
The inputDirection variable is responsible for controlling which direction the snake is going in depending on which key has been pressed. It will be used in a later method.
bool hasSnakeGrown = false
bool isSnakeDead
These bools are true/false statements for certain conditions, and is used to call functions only if the specified value of the bool is set (do this thing if true, otherwise do this other thing if false). The hasSnakeGrown bool is for allowing the script to check if the snake has grown, and the isSnakeDead bool is for allowing the script to check if the snake has died, presumably from running into itself.
[SerializeField] Camera MainCamera;
[SerializeField] AnimationClip CameraShake
This set of variables is for the game over sequence, which is animated. AnimationClip variable is new as well, in that I've never used this in any previous projects, however I did use this extensively in pre-SAE projects. This AnimationClip variable is for an animation file, and it is used in the script to make sure the animation file is attached to the Main Camera's animator before playing.
public UnityEvent snakeDeath
This is one of the most important variables in terms of project requirements. This is a UnityEvent variable, which allows you to create events in the game by setting it up in the inspector and then invoking it in the script. In this case, it's used for the death of the snake.
public int Score = 0
public int FirstDigitScore = 0
public int SecondDigitScore = 0
public int ThirdDigitScore = 0
public int FourthDigitScore = 0
These variables are responsible for making the score counter work. The score variable is the overall score for the game, and is what all four variables below it rely on. Speaking of which, each of the four variables below it are for each digit on the score counter and they are designed to only hold values up to 9 before being reset back to 0 at 10 (except for the FirstDigitScore).
SECTION 2: Starting everything up
This section is where the first steps of the script are taken to get everything up and running.
void Start()
The Start void calls the functions under it as soon as the game starts (provided the script is enabled in the first place) and then never again, unless the script is restarted.
inputDirection = moveRightOffset
This line sets which direction the snake should move in as soon as the game starts. In this case, inputDirection is set to the value of moveRightOffset, which means that the snake will move to the right as soon as the game starts before any input is detected.
Cursor.visible = true
This function simply allows the cursor to be shown while the game is running, which will allow the player to click the "Quit" button at the bottom of the screen if they want to quit the game.
InvokeRepeating(nameof(MoveSnakeEveryHalfSecond), 0.5f, 0.5f)
This function invokes a specified method in the script and sets it to repeat indefinitely until stopped with CancelInvoke(). The two values after it represent the delay until the specified method is invoked for the first time and the interval of the invoke when repeating. In this case, the MoveSnakeEveryHalfSecond method will be invoked half a second after the game starts and then will repeat every half second.
A flow chart that shows the order of operations for each function in this section.
SECTION 3: Making the snake move
This part of the script is what makes the snake move around in the game space.
void MoveSnakeEveryHalfSecond()
This is the name of the void that allows the snake to move around in the game space. This is also what's called by the InvokeRepeating function explained above.
if (hasSnakeGrown)
{
hasSnakeGrown = false
}
else
{
snakeTail.position = snakeBody[snakeBody.Count-1].position
}
In this conditional statement, the script checks if the snake has grown by checking the hasSnakeGrown boolean that another method will have altered (that will be talked about in section 5), and if it has been set to true, it sets the boolean as false; if it finds that it's been set as anything else (as in false, because a boolean is binary, therefore it can only be true or false), it will set the position transform of snakeTail as the position transform of snakeBody, and then will subtract one transform from the snakeBody list.
for (int i = snakeBody.Count-1; i > 0; i--)
{
snakeBody[i].position = snakeBody[i - 1].position
}
This section creates a for loop, which executes code for a certain amount of times until the condition it was set for has changed. For loops have the following syntax to them:
Initializer—mostly used to declare a variable that will be used in the loop;
Condition—this is the expression that the loop looks at to check if it should keep running. If the expression equals true, the loop will keep running, otherwise it will stop the loop; and
Iterator—which is commonly used for incrementing the variable declared in the initializer.
In this loop, if the i integer is more than 0, meaning it's true, then it will deduct a transform from its list.
snakeBody[0].position = snakeHead.position
snakeHead.position += inputDirection
snakeHead.position = CheckBoundary(snakeHead.position)
This final part of this section sets the first part of the index of the list for snakeBody to the position of snakeHead, and then adds inputDirection to the snakeHead position to take into consideration which direction the player wants to move the snake in, before finally calling CheckBoundary to check if the snakeHead has hit the boundary of the game.
A flow chart that shows the order of operations for each function in this section.
SECTION 4: Giving the player control over the snake
This section allows the player to take control of the snake by changing the direction it's headed in, as well as creating consequences of the wrong decision the player may make.
void Update()
The Update void calls all the functions under it for every frame, indefinitely, until the script is disabled.
if (isSnakeDead == false)
{
InputDetection()
}
else if (isSnakeDead == true)
{
CancelInvoke()
snakeDeath.Invoke()
}
This conditional statement checks the isSnakeDead boolean, and if it find that isSnakeDead is false, it will go to the InputDetection method (which will be explained later in this section), allowing the player to take control of the snake. If it finds that isSnakeDead is true, it will stop all repeating methods (in this case, only MoveSnakeEveryHalfSecond) and will invoke the snakeDeath Event.
A flow chart that shows the order of operations for each function in this section.
void InputDetection()
This is the method that the conditional statement above calls if it finds that isSnakeDead equals to false. We will talk about what this method does.
if (Input.GetKeyDown(KeyCode.UpArrow))
{
inputDirection = moveUpOffset
}
else if (Input.GetKeyDown(KeyCode.DownArrow))
{
inputDirection = moveDownOffset
}
This section of the code is what gives the player control over which direction the snake moves in. The code checks to see what key the player has pressed and sets the value of inputDirection as the move offset that corresponds with the correct direction. In this case, if the player pressed the up arrow key, the code will set inputDirection to equal moveUpOffset, which will make the snake move up in its next movement increment, otherwise if the player hits the down arrow key, it will do the same to make the snake move down. The same goes for the left and right arrow keys.
A flow chart that shows the order of operations for each function in this section.
This is the snakeDeath Event that is invoked when the script sees that the isSnakeDead bool is true. Events are a series of instructions that the game will carry out, and in this case, when the Event is invoked, it will run through the public Death method in the script.
public void Death()
{
StartCoroutine(SnakeBodyDZHit())
}
In the public Death method, it only has one line, which is to start the Coroutine named "SnakeBodyDZHit" (which we'll talk about after this). The reason why this method exists only to do this is because of the way IEnumerators work. Because IEnumerators cannot be public and can therefore only be used in the script, there's no way for this method to be called in the snakeDeath Event, which means that in order to start the Coroutine, you have to create a public void that the Event can call which starts the Coroutine.
IEnumerator SnakeBodyDZHit()
{
MainCamera.GetComponent<Animator>().enabled = true
yield return new WaitForSeconds(1)
RestartGame()
}
This is the IEnumerator that is called. The main reason why IEnumerator is used here is for the WaitForSeconds function, which I'll get to in a second. When the Coroutine is started, the method first enables the Animator component attached to the Main Camera, which will automatically play the animation that is attached to it. The method will then wait one second according to the WaitForSeconds function, which allows the animation to play through and then give the player some time to realise what's just happened, before then calling the RestartGame method.
void RestartGame()
{
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex)
}
This method is simple in that is only has one function—to simply restart the game. The way the function works is the script will access the Scene Manager to get the currently active scene and then build the scene from the index, which basically means to rebuild the level from the point that it was first built.
A flow chart that shows the order of operations for each function in this section.
SECTION 5: Making the apple work
This section is for the second of the most important part of the game: the apple (as well as the score counter).
void OnTriggerEnter2D(Collider2D other)
This method triggers only when it detects that another collider has entered its trigger collider.
if (other.CompareTag("Apple"))
{
Debug.Log ("Trying to eat apple!")
Vector3 newApplePos = new Vector3()
newApplePos = new Vector3(Random.Range(-7.2f, 7.3f), Random.Range(-2, 3), 0)
other.transform.position = newApplePos
The script checks to see if the object that has just entered its collider trigger has the tag "Apple" attached to it. If this is the case, the first thing it will do is report "Trying to eat apple!" to the console. Next, it will create a new Vector3 called newApplePos, and then using this new Vector3, it will find a random point within the space of the coordinate ranges specified (first range is on the X axis, second is on the Y axis), before finally teleporting the apple to that random point.
GameObject newSnakeBodyPart = Instantiate(snakePrefab, snakeBody[snakeBody.Count - 1].transform.position, Quaternion.identity)
hasSnakeGrown = true
snakeBody.Add(newSnakeBodyPart.transform)
AddScore()
The rest of the code for this conditional statement is for creating a new snake body part and adding to the score. Firstly, a new GameObject under the name of "newSnakeBodyPart" is created, and using this newly created GameObject, the script instantiates a new snake body part and sets it to the position of the part before it, before subtracting one from the list and then making sure the rotation is set to 0 with Quaternion.identity. The script then sets hasSnakeGrown to true (which the code in section 3 looks at), then it will add the transform of the newSnakeBodyPart GameObject to the snakeBody list, and then calls the AddScore method, which we'll get to below.
A flow chart that shows the order of operations for each function in this section.
void AddScore()
This is the method that is called for adding up the score and modifying the score counter.
Score++
FourthDigitScore++
Debug.Log("Added a point to the fourth digit")
The first three things this method does is add to the overall score variable and then add to the FourthDigitScore variable, which will work with the ScoreCounter.cs script to change the fourth digit in the game to add one, before reporting what just happened to the debug console.
if (Score == 10 || Score == 20 || Score == 30 || Score == 40 || Score == 50 || Score == 60 || Score == 70 || Score == 80 || Score == 90)
{
ThirdDigitScore++
FourthDigitScore = 0
Debug.Log("Added a point to the third digit")
}
This is the first condition statement in this method, and this condition statement checks to see if the score has hit any values to the power of 10 and modifies the third digit on the score counter with its own ThirdDigitScore variable according to the condition. If the condition criteria has been met, the value of ThirdDigitScore will be increased by one, the FourthDigitScore variable will be reset to 0 and then report the addition to the ThirdDigitScore variable to the debug console.
The condition statements are similar for the SecondDigitScore (which checks for values to the power of 100) and FirstDigitScore (which checks for values to the power of 100).
A flow chart that shows the order of operations for each function in this section.
This code continues in ScoreCounter.cs, which is the script that looks at the variables for the individual digits' scores and then directly modifies the score counter shown in the game. You can view that with the button below.
SECTION 6: Enforcing the game boundary
This part of the script checks to make sure that the snake is within the boundaries of the game (which is within the borders inside of the Nokia screen) so that the snake doesn't go off the screen.
Vector3 CheckBoundary(Vector3 pos)
This is the Vector3 method that is called by section 3 to check and see if the snake has hit the game boundary.
if (pos.x > 7.5f)
return new Vector3(-7.2f, pos.y, 0)
This method checks to see if the position of the transform variable has exceeded a specific position in the worldspace (basically when it touches the border), and if it finds that it has, it will then set the position to the opposite end of the boundary (where the other border is, to simulate teleporting).
This code continues for all four borders in the game space.
return pos
Lastly, the method returns the Vector3 position that has been calculated.
A flow chart that shows the order of operations for each function in this section.
SECTION 7: User convenience
This last section adds an extra function, the Quit button, for the convenience of the player, so they don't have to hit Alt + F4 or use Task Manager to close the game.
public void QuitGame()
This is the method that is called to quit the game, once the button in the game has been pressed.
This method is invoked through an Event, which is attached to the Quit button on screen and is invoked when the player clicks the button.
#if UNITY_EDITOR
{
EditorApplication.isPlaying = false;
}
#else
{
Application.Quit();
}
#endif
This method is pretty simple in its function, and it uses the technique that project 1 uses where it also works in the Unity Editor. In this method, the code checks to see if the game is being run in the Unity Editor, and if this is the case, it will try to exit playmode and return to the Editor, and if it found that it wasn't being found in the Editor and was being run in a standalone application, it will try to quit to the desktop.
A flow chart that shows the order of operations for each function in this section.
The full script
This the full SnakeTweaked.cs script, straight from the project. This version of the script includes the original colouring and also has the original comments that were removed from the code above.