それでは今回のゲームのメインとなるC#スクリプトを作っていきます。といってもメインスクリプトは全部で220行くらいしかないので、要所要所で切り分けて解説していきますね。
ライツアウトのC#スクリプト
はじめにスクリプト全体を掲載します。少し長いですが、上から順に眺めていってどんな処理が行われているのかを想像してみてください。
using UnityEngine;
using UnityEngine.UI;
public class UniLightsMain : MonoBehaviour
{
[SerializeField]
int minSize = 3;
[SerializeField]
int maxSize = 9;
[SerializeField]
int randomCount = 5;
[SerializeField]
GameObject lightPrefab;
[SerializeField]
Transform lightParent;
[SerializeField]
GridLayoutGroup grid;
[SerializeField]
Color onButtonColor;
[SerializeField]
Color onButtonHighlightedColor;
[SerializeField]
Color offButtonColor;
[SerializeField]
Color offButtonHighlightedColor;
[SerializeField]
Text clearText;
bool[,] lightStatus;
GameObject[,] lightObjects;
//ランダムに問題を生成する処理
public void CreateProblem()
{
clearText.enabled = false;
foreach (GameObject light in lightObjects)
{
Button button = light.GetComponent<Button>();
button.interactable = true;
}
lightStatus = new bool[lightStatus.GetLength(0), lightStatus.GetLength(1)];
int roopCount = Random.Range(2, lightStatus.Length);
randomCount = Mathf.Max(randomCount, 1);
for(int i = 0; i < roopCount; i++)
{
int choosedRow = 0;
int choosedCol = 0;
//乱数に「コク」を加える
for (int j = 0; j < randomCount; j++)
{
choosedRow += Random.Range(0, lightStatus.GetLength(0));
choosedCol += Random.Range(0, lightStatus.GetLength(1));
}
choosedRow /= randomCount;
choosedCol /= randomCount;
SwitchLights(choosedRow, choosedCol, true);
}
SetLightColor();
}
//ライトを生成する処理
public void CreateLights(int size)
{
size = Mathf.Clamp(size, minSize, maxSize);
clearText.enabled = false;
lightStatus = new bool[size, size];
lightObjects = new GameObject[size, size];
grid.constraintCount = size;
for (int i = 0; i < size; i++)
{
for(int j = 0; j < size; j++)
{
GameObject button = Instantiate(lightPrefab);
button.transform.SetParent(lightParent);
button.transform.localScale = transform.lossyScale;
LightButton light = button.GetComponent<LightButton>();
light.row = i;
light.col = j;
lightObjects[i, j] = button;
}
}
CreateProblem();
}
public void ClearLights()
{
if(lightObjects == null || lightObjects.Length <= 0)
{
return;
}
foreach(GameObject buttonObj in lightObjects)
{
Destroy(buttonObj);
}
}
//ライトを押したときの処理
public void SwitchLights(int row, int col, bool createMode)
{
if (lightStatus == null || lightStatus.Length <= 0)
{
return;
}
row = Mathf.Clamp(row, 0, lightStatus.GetLength(0));
col = Mathf.Clamp(col, 0, lightStatus.GetLength(1));
//その要素自身
lightStatus[row, col] = !lightStatus[row, col];
//上
if (row - 1 >= 0)
{
lightStatus[row - 1, col] = !lightStatus[row - 1, col];
}
//下
if(row + 1 < lightStatus.GetLength(0))
{
lightStatus[row + 1, col] = !lightStatus[row + 1, col];
}
//左
if(col - 1 >= 0)
{
lightStatus[row, col - 1] = !lightStatus[row, col - 1];
}
//右
if(col + 1 < lightStatus.GetLength(1))
{
lightStatus[row, col + 1] = !lightStatus[row, col + 1];
}
if (!createMode)
{
SetLightColor();
CheckClear();
}
}
//クリア判定
void CheckClear()
{
if (lightStatus == null || lightStatus.Length <= 0)
{
return;
}
foreach (bool status in lightStatus)
{
if(status == true)
{
return;
}
}
foreach(GameObject light in lightObjects)
{
Button button = light.GetComponent<Button>();
button.interactable = false;
}
clearText.enabled = true;
}
//ライトの色設定処理
void SetLightColor()
{
if (lightStatus == null || lightStatus.Length <= 0 || lightObjects == null || lightObjects.Length <= 0)
{
return;
}
Button button;
for(int i = 0; i < lightStatus.GetLength(0); i++)
{
for(int j = 0; j < lightStatus.GetLongLength(1); j++)
{
button = lightObjects[i, j].GetComponent<Button>();
ColorBlock colorBlock = button.colors;
if (lightStatus[i, j])
{
colorBlock.normalColor = onButtonColor;
colorBlock.pressedColor = onButtonColor;
colorBlock.selectedColor = onButtonColor;
colorBlock.highlightedColor = onButtonHighlightedColor;
}
else
{
colorBlock.normalColor = offButtonColor;
colorBlock.pressedColor = offButtonColor;
colorBlock.selectedColor = offButtonColor;
colorBlock.disabledColor = offButtonColor;
colorBlock.highlightedColor = offButtonHighlightedColor;
}
button.colors = colorBlock;
button.interactable = true;
}
}
}
}
変数の説明
まず説明が必要そうな変数について、上から順に少し補足しておきます。
- randomCount:問題を生成するときに乱数に「味」を出すための設定値です(※詳しいことは後述)。
- lightStatus:各ライトパネルのON/OFF状態を格納するための2次元配列です。
- lightObjects:各ライトパネルのゲームオブジェクトの情報を格納するための2次元配列です。配列のサイズはlightStatusと同じ。
処理の流れ
上記のスクリプトでは次のような流れで処理を行っています。
新しい問題を生成する場合の処理
- 全てのライトパネルをいったん削除する。
- ライトのオブジェクトの生成。プルダウンメニューで指定したサイズだけライトパネルを生成する。
- 問題の生成。ランダムな位置のライトと、そのまわりのライトのON/OFFを反転させる。
- ライトの色の反映。lightStatusを見て、各ライトの色を変更する。
ゲーム中の処理
- ライトパネルが押されたら、押された位置のライトとそのまわりのライトのON/OFFを反転させる。
- クリア判定。もしlightStatusを見て、すべてのライトがOFFならクリアテキストを表示する。
各処理の解説
CreateProblemメソッド
ランダムに問題を作成します。lightStatusを初期化してから、いくつか適当なライトをONにしておきます。
さて、ここで普通に考えると一マスずつON/OFFを切り替えて問題を作ればいいような気がします。しかしそれだと絶対にクリアできない(=解なし)問題になってしまう確率がかなり高いという問題があります。どうすれば良いでしょうか?
答えは簡単で、実際に遊ぶのと同様に「対象のライトと、その上下左右のライトもON/OFFを切り替える」ようにすればいいのです。こうすることで解なしになってしまう問題ができるのを避けることが可能です。
なお2重になっているforループの部分には、先述の乱数に味を加える処理を行っています。この辺の処理は「乱数にコクを出す方法について」という話を参考にしました。とても面白いのでぜひ見てみてください。もちろん完全にランダムな乱数でもいいのですが「自然な偏り」があったほうがいいかな?と思ったのでこのような処理を追加してあります(※別にやらなくてもいい処理かもしれませんが…)。
CreateLightsメソッド
ライトパネルのプレハブを複製(インスタンス化)してライトパネルのゲームオブジェクトを作っていきます。プレハブをInstantiateした後にSetParent()メソッドでパネル整列用オブジェクトの子に設定しています。このとき、そのままだとライトパネルのスケールがとんでもなく大きくなってしまうので「button.transform.localScale = transform.lossyScale;」によってスケールをリセットしています。
ClearLightsメソッド
ライトパネルをすべて削除するメソッドです。
SwitchLightsメソッド
ライトパネルがクリックされた時の処理を行います。クリックされたパネル自身と、その上下左右のパネルのON/OFFを反転させます。もし範囲がはみ出た場合はその部分の処理をスキップします。
CheckClearメソッド
クリア判定を行います。
SetLightColorメソッド
ライトの色を設定します。
メインスクリプトのアタッチ
スクリプトを書いたら、空のゲームオブジェクトを作ってそこにこのスクリプトをアタッチしてください。そのままだと色が設定されていないので忘れずに設定するようにしましょう。これでライツアウトの主な処理が一通り完成しました。
次のページ→UI用スクリプトの作成
