teaching machines

Honors 104.502 Lab 4 – Lights Out

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

In this lab we’ll explore mechanisms for handling mouse clicks, staging multiple levels, and working with 2D grids of game objects.

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.

Bulb Image

Lights Out is played on a 5×5 grid of lights that turn on and off. We’ll represent a light with a single image—but we’ll use some magic to toggle between colors to represent the on and off states. Quickly create a square image of a light in a drawing program of your choice. (I drew a lightbulb; you do as you please. But don’t deliberate. This is only a prototype. The image is easily changed later.) Make the background transparent (preferably) or black. Draw the light in white. The color is important.

Import the image into Unity. Drag the image from assets to the hierarchy. Name it something meaningful.

We want the light to consume a 1×1 area in our game world, so we need to tell Unity how to scale the image’s pixel dimensions into world units. Click on the light image in the assets. Enter the image’s width or height in pixels in the Pixels Per Unit box. Hit Apply.

Add a BoxCollider2D that we’ll use later to detect mouse clicks on the image.

Does the image fit into a grid cell?

Camera

Since we’re dealing with a 5×5 grid, let’s arrange our camera and game screen to match.

First, set your light image’s position to 0, 0, 0. Drag it to the assets to make it a prefab and then make another instance of the prefab at 4, 4, 0. These two instances will serve as a frame of reference for our next steps.

Now, let’s adjust the camera. Change the camera’s size to 2.5 in the Inspector. Size is the camera’s “radius.” Its diameter is now the desired 5. Adjust the camera’s position so that it just fits around the two light images. What must you translate by?

Play your game. What happens?

Let’s also fix the game screen to project our camera view onto a squarea—a portmanteau of square and area. In the game window, click on the resolution dropdown menu on the top left. Click +, label the new resolution “Square”, set the Type to Aspect Ratio, and set the width and height to 1.

When you play, do the light images appear at the corners and without distortion?

Bulb Controller

Now let’s make the bulb change color when clicked on. Click on the light prefab. Notice that SpriteRenderer component in the Inspector. Try changing its color. What effect does this have?

We want to programmatically change the light’s color in a script. Add a LightController script to the light prefab. Let’s start by getting a handle on the SpriteRenderer in the code:

private new SpriteRenderer renderer;

void Awake() {
  renderer = GetComponent<SpriteRenderer>();
}

Note that we assigned renderer in Awake instead of Start. Awake is run a bit earlier than Start, which will be important later.

Add a public isOn variable. Of what type should this be, based on its name?

Add functions TurnOn and TurnOff. Place the following two lines inside each, filling in appropriate values for the ?s:

isOn = ?;
renderer.color = Color.?;

The most common colors are predefined in the Color class under their standard names. (One can make custom colors too.)

Call TurnOff in Awake to initialize its state.

Also add a Flip function. If the light is on, turn it off. If off, turn it on. Don’t redo work you’ve done in the functions above. Call the functions instead.

Lastly, add an OnMouseDown function. This is a callback function that Unity knows about. When Unity detects a mouse click within the image’s collider, it will call this function. Flip the light when it’s clicked.

Does it work?

Board Controller

Individual lights are great, but lights in this game are not so independent. When we click on a light, it and all of its neighbors flip their states. Handling this multi-light logic is something best done at a higher level than an individual light’s Flip function. Let’s add a BoardController script to the main camera.

This script will maintain a 2D grid/array of the LightControllers. Let’s declare a private variable and initialize it:

private LightController[,] lights;

void Start() {
  lights = new LightController[5, 5];
}

Checkpoint 2

Person B types.

Board Generation

Instead of assembling the board by hand in the scene editor, we’ll use code to dynamically instantiate our lights.

First, remove your two light instances from the scene, but keep the light prefab in the assets. Add a public GameObject for the light prefab to your BoardController.

Now, let’s add some loops to visit each cell of our board:

for (int r = 0; r < 5; ++r) {
  for (int c = 0; c < 5; ++c) {
    // (c, r) is the center of our cell
  }
}

Inside the loops, we can instantiate our light prefab at the current cell:

GameObject lightInstance = (GameObject) Instantiate(lightPrefab, new Vector2(c, r), Quaternion.identity);

We also want to grab hold of the light’s controller and record it in our array for later use:

LightController lightController = lightInstance.GetComponent<LightController>();
lights[r, c] = lightController;

Does the complete 5×5 board appear when you play?

Flipping Neighbors

Our game logic is now a little distributed. LightController manages a single light. BoardController manages the grid of them. They’ll have to do some talking with each other to make our game happen.

Let’s give LightController a reference to the BoardController and some ints reporting the light’s grid position. Add these publics to LightController:

public BoardController boardController;
public int x;
public int y;

Let’s assign these back in BoardController inside the loops:

lightController.boardController = this;
lightController.x = c;
lightController.y = r;

Also in BoardController, add a FlipAt function for flipping a light and its neighbors:

public void FlipAt(int x, int y) {
  // flip lights[y, x]
  // flip right neighbor, if there is one
  // flip left neighbor, if there is one
  // flip top neighbor, if there is one
  // flip bottom neighbor, if there is one
}

Replace the comments with code to accomplish the specified task. Recall that you have code in LightController that can flip a light’s state.

Back in LightController, alter OnMouseDown function to let the board do the flipping instead of the light itself:

void OnMouseDown() {
  boardController.FlipAt(x, y);
}

Does the flipping mechanic work when you playtest?

Level Configurations

All the pieces for our game are in place. Now it’s time to design some levels. We’re going to take the simplest approach possible for designing levels: we’ll describe the board layout for each level in a text file. For example, here’s a really simple level:

.....
..x..
.xxx.
..x..
.....

Where do we put this? First, create a directory named Resources in your project assets. Then, find your game folder on your computer through your operating system’s file manager. We can’t do this through Unity, because Unity doesn’t let us create simple text files.

Using a simple text editor like NotePad or TextEdit, copy the board above into a text file in Resources named plus.

Back in BoardController, declare a public TextAsset array and an index into this array representing our current level:

public TextAsset[] levels;
private int iLevel = 0;

In Start, add this line before your loops to open the text file and split it up into an array of five lines:

string[] lines = levels[iLevel].text.Split('\n');

The \n represents the linebreak character.

Inside your loop, after you’ve got a lightController reference, turn the light on if there’s an x in the corresponding cell of the layout:

if (lines[r][c] == 'x') {
  lightController.TurnOn();
}

Back in the Inspector, resize this array to have one element. Drag the level1 file from Resources into the empty slot.

Does the plus sign automatically show up in the lights when you play? Make some other simple layouts in your Resources directory. Expand your array and add them in.

Win Logic

All that remains of the core game logic is checking for a winning state. The player wins when all the lights are off. Where’s a good place to check if this is true?

Right before the FlipAt method finishes seems like a good place to check. Add this code there, which traverses the board and tries to find at least one light on. If we find none on, let’s advance to the next level by incrementing our level index and loading in the new layout:

bool allAreOff = true;
for (y = 0; y < 5; ++y) {
  for (x = 0; x < 5; ++x) {
    if (lights[y, x].isOn) {
      allAreOff = false;
    }
  }
}

if (allAreOff) {
  Debug.Log("You won level " + iLevel + "!");
  iLevel = iLevel + 1;
  Start();
}

You may wish to use a more elegant and less abrupt means of transitioning between levels. A delay via a coroutine or user interface widgets might help.