Honors 104.502 Lab 4 – Lights Out
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 LightController
s. 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 int
s 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.