Honors 104.502 Lab 2 – Snake
When people start making games, they usually start with TicTacToe. I can’t do that to you. TicTacToe, like War, gives the illusion of player interaction but is far too deterministic. Today we’ll implement the second game that many people make: Snake.
Find a parter and make sure you have access to a computer with Unity installed.
Checkpoint 1
Let’s start with Person A at the computer.
Snake Head
Create the snake’s head using Unity’s builtin cube GameObjects. Give it a distinct material. We’re going to treat the game world as a grid of 1×1 cells, so snap it to whole number coordinates.
Snake Egg
Create an egg using Unity’s builtin sphere GameObjects. Give it a distinct material. Snap it to whole number coordinates.
Don’t forget about Duplicate. If you have a source object kind of like the one you want to make, it’s often easier to Duplicate and tweak the clone. Especially if you remember the keyboard shortcut: Control/Command-D.
Movement
Let’s make the head move. Start by adding a script to the snake head GameObject. This gives the snake head a new behavior/component that we get to write. The naming convention for scripts is ThingController
, where Thing
is replaced by the name of the entity you are controlling with this script.
In class we moved our objects with something like this in the Update
method:
float horizontal = Input.GetAxis("Horizontal");
float vertical = Input.GetAxis("Vertical");
transform.Translate(new Vector3(horizontal, vertical, 0));
The GetAxis
method gives us a value in [-1, 1] that indicates how much pressure is being applied to the WASD keys, cursor keys, or joystick. It nicely smooths these pressure readings out to prevent sudden discontinuities. However, sometimes we want discontinuities. In Snake, let’s make the snake move from very discretely from cell to cell of a grid. Here’s the idea we’re after in pseudocode:
if w, a, s, or d is down
offset = (0, 0, 0)
if w is down
offset.y = 1
else if a is down
offset.x = -1
else if s is down
offset.y = -1
else
offset.x = 1
translate by offset
Let’s rewrite this in C# in your update method. You’ll need to know a few things to do this. First, instead of Input.GetAxis
, let’s use Input.GetKeyDown
. Feed it a string for the key you want to know about. It will give back true or false, depending on the “downness” of the key. Second, to create a new 3-vector variable at the origin, we say something like this:
Vector3 offset = new Vector3(0, 0, 0);
Third, an if
statement has this form in C#:
if (some true/false expression) {
// code to execute when condition is true
}
When we’re choosing between two actions, the code looks like this:
if (some true/false expression) {
// code to execute when condition is true
} else {
// code to execute when condition is false
}
When we’re choosing between three or more actions, the code looks like this:
if (expr1) {
// code to execute when expr1 is true
} else if (expr2) {
// code to execute when expr1 is false and expr2 is true
} else if (expr3) {
// code to execute when expr1 and expr2 are false and expr3 is true
} ...
...
} else {
// code to execute when none of the above conditions is true
}
Watch your curly braces and indentation! Keeping the code readable is important to understanding what you’ve written.
Playtest. Does the snake jump from cell to cell?
Triggers
When the snake runs into an egg, we want it to grow. When it runs into itself, we want game over. To detect these collisions, we need to add some tags and colliders to our objects.
Click on the egg. In the Inspector, click on the Tag button and add tags Egg
and Segment
. Click on the egg again and assign the Egg
tag to the egg.
Remove the 3D sphere and box colliders from the egg and snake head. Add equivalent 2D colliders to each. Also add a Rigidbody to the snake head.
A component with a Rigidbody is normally managed by the physics system. We want more control. Check Is Kinematic on the snake head to declare that we are controlling its position, not the physics engine.
By default, colliders act as walls that stop other objects from passing through. That’s not the behavior we want for the egg. Check Is Trigger on the circle collider to make it act more like a switch than a wall.
Now, in the script you’ve started, we should be able to detect when the snake head lands on something with a trigger collider with OnTriggerEnter2D
:
void OnTriggerEnter2D(Collider2D collider) {
Debug.Log(collider.gameObject.tag);
}
If we hit an egg, let’s destroy it. How do we tell if we hit an egg? Let’s compare the tag with the known egg tag:
collider.gameObject.tag == "Egg"
This expression would make an excellent condition for an if
statement. If we hit an egg, let’s destroy it:
Destroy(collider.gameObject);
We’ll make the snake grow in the next checkpoint.
Checkpoint 2
Person B types.
Prefabs
We’re going to want a lot of eggs. Duplicate isn’t a great idea here. If we need to a change a property of the eggs, we’ll need to change each one individually. Instead, let’s drag the egg from their hierarchy into the project assets. This makes its a prefab.
To add a second instance of this prefab, drag it from the project assets into the scene. And a third and fourth. And so on. Any changes made to the prefab will affect the instances.
Body Segments
Let’s make a snake body segment. Duplicate the snake head and rename it to something meaningful. Remove the Rigidbody and controller script, as these only apply to the snake head. Make it a trigger. Give it tag Segment
.
We’re going to want many instances of it, so make a prefab out of it and delete it from the scene.
Segments List
Somehow we’re going to need to manage a list of snake body segments. This calls for some sort of data structure that can hold together a list of GameObjects. We’ll use what’s called a LinkedList
. We’ll need to do a few things in your script to make this work.
At the top of the file, add this line after the similar lines already there:
using System.Collections.Generic;
Within the outermost set of curly braces (where we’ve been adding public variables), add a declaration for our list of segments:
public class HeadController : Monobehavior {
private LinkedList<GameObject> segments;
void ...
}
In the Start
function, we need to give this list its initial assignment:
void Start() {
segments = new LinkedList<GameObject>();
}
Moving
When the snake moves, we want to do two things—provided the snake has a body at all:
- Introduce a new body segment where the head just was.
- Remove the tail.
When the snake lands on an egg, we want to add a new body segment wherever the tail just was—or, if it doesn’t have a body, where the head just was. Let’s rig this logic up in Update
:
on update
if one of WASD down
offset = ...
oldPosition = transform.position
translate by offset
if snake has a body
prepend new segment at oldPosition
tailPosition = segment.last
remove tail
else
tailPosition = oldPosition
To rewrite this in C#, we need to know a few things:
- To ask if the snake has a body, we can use the expression
segments.Count > 0
. - To create a new segment, we need to know first what prefab we want to make an instance of. At the top of your class, declare a public like this:
public GameObject segmentTemplate;
Select your snake head, find your script in the Inspector, and drag your segment prefab onto the
Segment Template
input. - To make a new segment instance at the position where the head just was, say something like this:
GameObject newSegment = (GameObject) Instantiate(segmentTemplate, oldPosition, Quaternion.identity);
- To prepend the new segment onto the list, write this:
segments.AddFirst(newSegment);
- To remove the tail but remember where it was located, write this:
GameObject tail = segments.Last.Value; segments.RemoveLast(); // remove from our list tailPosition = tail.transform.position; Destroy(tail); // remove from Unity's memory
-
oldPosition
andtailPosition
will need to be declared.oldPosition
will only be needed insideUpdate
, so it can be declared there:Vector3 oldPosition = ...;
tailPosition
is going to be referenced onOnTriggerEnter2D
, so we’ll need to declare it up top, outside any particular function. It’s a good idea to make it private so it can’t be fiddled with in the Inspector:private Vector3 tailPosition;
Growing
If we’ve set tailPosition
correctly, we should be able to make a new segment when we hit an egg in OnTriggerEnter2D with this line:
segments.AddLast((GameObject) Instantiate(segmentTemplate, tailPosition, Quaternion.identity));
Self-collision
Expand OnTriggerEnter2D to also handle collisions with segments. When the snake collides with itself, we can delete the whole thing with this code:
foreach (GameObject segment in segments) {
Destroy(segment);
}
segments.Clear();
Destroy(gameObject);
Beyond
If you’re looking into more challenges:
- Add sounds using
AudioClip
s andAudioSource
. The program sfxr is pretty great for generating gratifying low quality sound effects. - Add walls.
- Make or find images for the egg and snake body. Bring them into your scene as sprites, and use them instead of the builtin GameObjects.
- If you want to add an automatic timer, talk to me about coroutines.