Honors 104.502 Lab 3 – Snake Cont’d
In this lab we’ll add some final features to the snake game:
- audio
- random egg placement
- walls
- continuous motion
But first, if you didn’t get your work from last lab checked off, do so in the first 15 minutes of this lab.
Checkpoint 1
Person A types.
Sound Effects
Download sfxr, a free 8-bit sound effect generator. Mac users, follow the “cool Mac port” link. Generate and export a pickup sound to play when we encounter an egg and a hurt sound to play when we run into an obstacle.
In Unity, import these two audio clips.
When the snake head enters a trigger collider, we want to play one of these sound effects. Add an Audio Source to to the snake head. Just inside the controller class, declare a reference to the audio source. The outside world doesn’t need to access it, so make it private:
private AudioSource sounder;
This variable needs to be assigned something. That assignment needs to be done only once, not every frame. This, then, is a job for the Start
function. To access the script’s sibling Audio Source component, we write something like this:
void Start() {
// previous code
sounder = GetComponent<AudioSource>();
}
Add two public variables to the controller of type AudioClip
. Give them meaningful names. Drag your two clips from the project assets into the controller’s new inputs in the Inspector.
Now find your code for handling collisions. Play a clip with something like this:
sounder.PlayOneShot(myAudioClip);
But use your meaningful names.
Test your code. Does it do what you want? Do both clips play?
Slow Death
You probably found that the hurt sound effect doesn’t play. Your code that destroys the snake when it collides with a segment deletes the entire snake game object, Audio Source and all.
Let’s handle the snake’s death in a different way. We’ll keep the snake’s carcass visible, but just stop handling user input. Comment out the code for destroying the snake and its body segments. In its place add this variable assignment:
isAlive = false;
Declare this up top as a private bool
. In Start
, assign it true
.
Now, in update, we only want to handle user input if the snake is alive. In your outermost if
statement, add a further clause to your condition. We’ll use the and ( &&
) operator (and some parentheses to enforce precedence) to require both subconditions to be true.
if (isAlive && (Input.GetKeyDown("w") || ...)) {
...
}
How does that work?
Random Egg Placement
Up till now, our eggs have been manually placed around our scene. Once they are eaten, the game is pretty much over. Let’s fix that by generating new eggs at random.
First, make sure you have an egg prefab in your project assets. If not, drag one of them from the hierarchy into the assets.
Next, delete all the eggs from your hierarchy.
In the controller, add a public eggTemplate
just like you have a segmentTemplate
. Drop the egg prefab into the input in the Inspector.
Now, let’s write a little function to generate a new egg:
void GenerateEgg() {
// make a new Vector2 for the egg position
// assign it a random x value
// assign it a random y value
// instantiate the new egg
}
Replace each of these comments with the code that accomplishes the desired task. You have done something similar to comments 1 and 4 elsewhere in your code. For comments 2 and 3, use Unity’s Random.Range function. Pick a range that will keep all eggs on the screen.
In Start
, call this function a couple of times:
GenerateEgg();
GenerateEgg();
Call this function again to replace an eaten egg.
Playtest. What do you think?
Checkpoint 2
Person B types.
Walls
When the snake goes off the game area, we want the snake to perish. There are many ways to do this. Let’s go this route:
- Our game area will have a scaled cube in its background.
- The cube will have a 2D box trigger collider.
- When we exit the collider, the snake perishes.
Add the cube to your scene. Give it an aesthetically pleasing material. Scale it to fit your camera and reachable game area. Remove the 3D box collider and give it a 2D one. Make sure its a trigger. Add and assign it a Background
tag.
In the controller, add an exit handler:
void OnTriggerExit2D(Collider2D collider) {
...
}
As you did in the enter function, have your snake perish when it exits a collider tagged Background
.
You may notice that there are two places in your code where a snake might perish. Instead of duplicating that code, it’s probably wiser to factor it out to a function:
void Perish() {
// your one-stop shop for all snake death-related matters
}
Then in your two places where the snake might die, just call this function.
Continous Motion
Right now our game is too easy. The only conflict is player vs. fingers. Let’s add the element of time, a beating pulse that drives the snake toward destruction. We want the snake to have some inertia. Once it moves in a certain direction, it will continue to move in that direction in the absence of player input.
Currently our movement is triggered solely by user input in the Update
function. We also want to move when a timer expires after, say, 1 second. As a first step, let’s factor out our movement code that we currently have in Update
to a new function we’ll call Move
. We’re not changing any code yet, just relocating it:
void Move() {
Vector3 oldPosition = ...
transform.Translate(...)
if (...) {
...
} else {
tailPosition = ...
}
}
In Update
, replace this code with a call to Move
. You should see an error about your offset vector not being visible. That’s because we declared the offset variable in Update
, not in Move
. To fix this, let’s make offset
a public variable at the top of your class:
public Vector3 offset;
Now it’ll be visible in both methods. Remove the type name from the declaration in Update
so you don’t declare a second vector named offset
:
offset = new Vector3(0, 0, 0);
To get automatic movement going, we need to write something called a coroutine, so-called because it runs alongside our normal game functions. Essentially, as long as the snake is alive, we want to keep it moving:
IEnumerator AutoAdvance() {
while (isAlive) {
yield return new WaitForSeconds(1);
Move();
}
}
Coroutines have a funny return type. It’ll move based on whatever the last value was for offset
.
We need to schedule this coroutine to run or stop running when we get user input. First, declare a private variable for it:
private Coroutine autoAdvancer;
If we don’t assign it anything in Start
, it’ll have a special value null
. In Update
, after we Move
based on the user input, let’s kick this coroutine off:
autoAdvancer = StartCoroutine(AutoAdvance());
When we get new user input, we want to suspend the previously scheduled movement. Just inside your outermost if, let’s stop any existing coroutine:
if (isAlive && ...) {
if (autoAdvancer != null) {
StopCoroutine(autoAdvancer);
}
offset = ...
}
That !=
is read “not equals”—meaning that only if autoAdvancer
was previously assigned a value do we try to stop it. Playtest!
Beyond
If you finish early, consider tackling these extra challenges:
- Generate eggs only in places where the snake is not present. A loop and
Physics2D.OverlapCircle
might help. - Embed walls/obstacles inside the game area.
- Adding a score text to the user interface.