teaching machines

Honors 104.502 Lab 2 – Snake

February 5, 2016 by . Filed under honors gamedev, labs, spring 2016.

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:

  1. Introduce a new body segment where the head just was.
  2. 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:

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:

  1. Add sounds using AudioClips and AudioSource. The program sfxr is pretty great for generating gratifying low quality sound effects.
  2. Add walls.
  3. 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.
  4. If you want to add an automatic timer, talk to me about coroutines.