Unityで簡単な2D脱出ゲームを作ってウェブサイトで公開してみよう 〜アイテムを使ったパスワード〜

この記事はだいぶ前に書かれたものなので情報が古いかもしれません
アイテムを使ったパスワード

この記事を三行にまとめると

for文を使って配列を回しています
隙を生じぬ二段構えにできます
だいたいこれらの応用で実装できるんじゃないかなあ
長かったパスワード作りも今回で最後となりました。

ということで今回はアイテムを使ったパスワードを実装します。あと本作にはないんだけど脱出ゲームでよく見かけるパターンのパスワードをこんな風に作ったらどうだ的な話もしたいと思うのでおなしゃす。

動画はこちらです。





アイテムをセットする

本作ではあみだくじに4枚の国旗パネルをセットするというパスワードを実装しています。

あみだくじのパスワード画面

基本的な考え方は今までのパスワードとそんなに変わらないのですが、テキストや画像の場合と主に違うのはアイテムを使用する処理が入るのとnows[n]を+1して内容を切り替えるわけじゃないってところですかね。でもnows自体は今回も使うので、まずは国旗パネル用の変数とnowsの初期値を設定します。

public class Door : Password {
  public Item[] panels;
  
  void Start () {
    nows = new int[]{4, 4, 4, 4};
  }
}

今回はnowsの初期値を0ではなく4としています。理由は後ほど。

「panels」は国旗パネルのアイテムスクリプトの配列です。型をItemとしていますが、これは各アイテムの親スクリプトです。何で親スクリプトにしているかと言うと、各アイテムのスクリプトはクラス名が異なるので、異なる変数を4つ用意しなきゃいけなくなるからです。

public class Door : Password {
  public America america;
  public Brazil brazil;
  public Ozbekiston ozbekiston;
  public Micronesia micronesia;
}

public class America : Item {}
public class Brazil : Item {}
public class Ozbekiston : Item {}
public class Micronesia : Item {}

こんな風に別々の変数で管理するとforeachなどでまとめて扱いにくいので今回は親クラスを使用しました。型が親のクラスでもインスペクターでセットするのはAmericaやBrazilなどのスクリプトがセットされているオブジェクトをドラッグ&ドロップすればオーケーです。正直な話、国旗のパネルはPanelという一つのアイテムスクリプトを使い回せばこんな不都合も起きなかったんですがね……まあやっちゃったもんはしかたないっす。

続いてパネルをセットするスクリプトです。

public class Door : Password {
  public Item[] panels;
  
  void Start () {
    nows = new int[]{4, 4, 4, 4};
  }

  public void SetPanel(int n) {
    for(int i = 0; i < panels.Length; i++) {
       if(panels[i].selected.activeSelf) {
         images[n].sprite = panels[i].image.sprite;
         nows[n] = i;
         panels[i].UseItem();
         return;
       }
    }
  }
}

今回はfor文を使って配列を回しています。for文もforeachと同じで繰り返し処理を行うものなのですが、foreachと違って自分で繰り返しの条件を設定する必要があります。初期値、条件式、変化量を設定して、条件式を満たしている間は処理を繰り返すというものです。

上記のfor文では「int i = 0」が初期値、「i < panels.Length」が条件式、「i++」が変化量となります。++は+1と同じです。つまり「初期値が0の変数iを+1し続けて、iがpanels配列の要素数(4)に達するまで処理を繰り返す」という内容になります。

これによってどんな処理が可能になるかと言うと、まずpanels[i]でpanels配列のi番目の要素を扱えるようになります。今回は選択中のパネルをセットするという動きをしなければならないので、配列の何番目のパネルが選択中なのかを知る必要があります。アイテム選択時にどのアイテムが選択されているかという情報をセットする仕組みを作っても良いと思うのですが、パネル以外のアイテムが選択された時に「どのパネルも今は選択中じゃない」みたいな処理も入れたりしなきゃいけないので、たぶん上記の方が実装の手間は少ないと思います。

「panels[i].selected」は各Itemスクリプトのselected変数です。ここにはアイテムの選択状態を示す画像オブジェクトが入っています。これがアクティブなパネルが現在選択中という判断になります。

アクティブなパネルがあったらそのパネルの画像(panels[i].image.sprite)をn番目のボタンの画像(images[n].sprite)にセットします。その際にnows[n]に「i番目のパネルをセットしたぞ」という情報を持たせるようにしています。これでテキストや画像の時と同じくn番目のボタンにどのパネルをセットしているかの情報を保持できます。

「panels[i].UseItem()」はi番目のパネルを使用済みにするための関数です。前にItemスクリプトの中に作ったやつです。

UseItemの下にreturnを書いていますが、例えば0番目(i = 0)のパネルが選択状態だった場合、1番目以降のパネルは全て未選択の状態になってるはずなので(なってなければバグ)、if文がtrueになることはありません。したがってそれ以上繰り返し処理を行う必要もなくなります。同様に1番目のパネルが選択状態の時も2番目以降の処理を行う必要はないので、そこで関数を終了させても何ら問題はありません。なのでパネルをセットできた時点でreturnで処理を終了させています。

この処理をforeachでやっちゃダメなのかと言うと別にそんなことはありません。ただ普通にforeach文を使うだけだと「今配列の何番目の処理を行なっているのか」という情報が取れません。

foreach(Item panel in panels) {
  if(panel.selected.activeSelf) {
    //今何番目?
  }
}

いずれかのpanelがアクティブになるところまでは判定できますが、条件がtrueになった時にそれが何番目のパネルかはこれでは分かりません。

だからforeachを使う場合でも自分で先ほどのiのような変数を用意する必要があります。

int i = 0;

foreach(Item panel in panels) {
  if(panel.selected.activeSelf) {
    //今i番目
  }

  i += 1;
}

これならfor文の時と同じようにループするたびにiの値が1ずつ増えていくので、今何回目の繰り返し処理を行なっているかが分かります。

あとは「LINQ」という機能を使うとforeachでもi番目に相当する情報が取れます。

using System.Linq;

public class Door : Password {
  public Item[] panels;

  public void SetPanel(int n) {
    foreach(var panel in panels.Select((obj, i) => new {obj, i})) {
       if(panel.obj.selected.activeSelf) {
         images[n].sprite = panel.obj.image.sprite;
         nows[n] = panel.i;
         panel.obj.UseItem();
         return;
       }
    }
  }
}

何かこんな感じ。SQLを使ったことがある人なら感覚をつかみやすいかもしれません。クエリの書き方が似てる。使いこなせるとforeachに新たな力を与えることができるみたいです。でも僕はまだLINQをほとんど使ったことがないので、これくらいだったらfor文で良いんじゃないかなあと思う。



パネルをセットし直す

パネルをあみだくじにセットする処理は完成しましたが、今はUseItemでパネルが使用済みになってしまうともうそのパネルは使用できなくなってしまうので、一度でも間違った場所にパネルをセットしてしまうとその時点で詰んでしまいます。なのでパネルだけは他のアイテムと違って使用済みの状態を解除する仕組みが必要になります。

そこでItemスクリプトに使用済みフラグを解除する関数を作ってみます。

public class Item : MonoBehaviour {
  public GameObject item;
  public GameObject selected;
  public GameObject target;
  protected bool used;

  public void UseItem(bool active) {
    if(selected.activeSelf) {
      target.SetActive(active);
      used = true;
      selected.SetActive(false);
      item.color = new Color(1, 1, 1, 0.4f);
    }
  }

  public void UsedCancel() {
    used = false;
    image.color = new Color(1, 1, 1, 1);
  }
}

「UsedCancel」という関数を作りました。本作ではアイテムが使用済みかどうかはusedという変数で管理しています。UseItem関数が実行されるとusedがtrueになりアイテムが使用済みという状態になるので、このusedをfalseに戻せばまた使用可能な状態になります。ついでに使用済みアイテムは半透明にしているので、不透明度も1に戻しておきます。

このUsedCancel関数をSetPanelから呼び出します。

public class Door : Password {
  public Item[] panels;
  
  void Start () {
    nows = new int[]{4, 4, 4, 4};
  }

  public void SetPanel(int n) {
    // UsedCancelを実行
    if(nows[n] != 4) {
      panels[nows[n]].UsedCancel();
    }

    for(int i = 0; i < panels.Length; i++) {
       if(panels[i].selected.activeSelf) {
         images[n].sprite = panels[i].image.sprite;
         nows[n] = i;
         panels[i].UseItem();
         return;
       }
    }
  }
}

「ボタンをクリックした時、そのボタンにいずれかのパネルがセットされていたらそのパネルに対してUsedCancel関数を実行する」というコードを追加しました。

パネルがセットされているかどうかはnows[n]が初期値かどうかで判定します。nows[n]が4以外の数字になっていればいずれかの国旗パネルの画像がセットされているはずだと判断し、UsedCancel関数が発動します。

ちなみに初期値が4になっているのは、0だと0番目のパネル(panels[0])をセットした時と同じ状態になってしまうからです。画像をそろえるパスワードの回でも少し触れましたが、初期値を0とした場合、このコードではnows[n]が0の時に初期値の0なのかpanels[0]をセットしての0なのかは判別ができません。なので4にしました。

別に4じゃないとダメなわけじゃなくて、ようはパネルをセットした時の数字とかぶらないようにさえなっていれば良いので、0〜3以外の数字なら何でもOKです。10でも100でも777でも9192631770でも良い。あるいは初期値の方を0にして、nowsの値はi番目+1の数字を入れるようにしても良いかもしれません。

public class Door : Password {
  public Item[] panels;
  
  void Start () {
    nows = new int[4];
  }

  public void SetPanel(int n) {
    if(nows[n] > 0) {
      panels[nows[n] - 1].UsedCancel();
    }

    for(int i = 0; i < panels.Length; i++) {
       if(panels[i].selected.activeSelf) {
         images[n].sprite = panels[i].image.sprite;
         nows[n] = i + 1;
         panels[i].UseItem();
         return;
       }
    }
  }
}

こっちの方がスマートかもね。UsedCancelの際にnows[n]の-1番目のパネルを解除するってところだけ見落とさなければ感覚的には分かりやすいかも。

初期値を0以外にする時は変数を一つ用意して、何らかの理由で値を変える事態になった時にその変数の値だけ変えれば良い状態にしておくのもスマートな書き方かもしれません。

public class Door : Password {
  public Item[] panels;
  private int def = 4;
  
  void Start () {
    nows = new int[]{def, def, def, def};
  }

  public void SetPanel(int n) {
    if(nows[n] != def) {
      panels[nows[n]].UsedCancel();
    }

    for(int i = 0; i < panels.Length; i++) {
       if(panels[i].selected.activeSelf) {
         images[n].sprite = panels[i].image.sprite;
         nows[n] = i;
         panels[i].UseItem();
         return;
       }
    }
  }
}

何にせよこれでパネルをセットし直す動きは実装できました。ただしこれではまだ完成一歩手前です。もうちょっとだから頑張ろう。

今のままだとどのパネルも選択されてない状態でパネルがセットされているボタンを押した場合、パネルをつけ替える処理は発生しないけどUsedCancelでパネルが使用可能な状態には戻ります。つまりセットされている状態のパネルがはずれないままもう一度使えるようになってしまい、複数のボタンに同じパネルをセットできるようになります。そういう仕様でも良いという場合もあると思いますが、今回は一枚のパネルを二ヶ所以上につける想定はしていません。

パネルが未選択の時にセット済みのボタンを押した時はUsedCancelを実行しないという処理をつけ加えても良いかもしれませんが、ここではパネルをはずす(初期状態に戻す)という動きを考えてみたいと思います。

public class Door : Password {
  public Item[] panels;
  public Sprite button; 
  
  void Start () {
    nows = new int[]{4, 4, 4, 4};
  }

  public void SetPanel(int n) {
    if(nows[n] != 4) {
      panels[nows[n]].UsedCancel();
    }

    for(int i = 0; i < panels.Length; i++) {
       if(panels[i].selected.activeSelf) {
         images[n].sprite = panels[i].image.sprite;
         nows[n] = i;
         panels[i].UseItem();
         return;
       }
    }

    // ボタンを初期状態に戻す
    images[n].sprite = button;
    nows[n] = 4;
  }
}

ボタンを初期状態に戻すコードをつけ足してみました。buttonはパネルがセットされてない状態のボタンの画像です。こいつを入れてやれば画像がデフォルトの状態に戻ります。nows[n]も初期値に戻しています。

for文の下にこの処理を書いたのは、いずれかのパネルが選択されているとfor文の中でreturnが発動するので、その時点で関数が終了して初期状態に戻す処理は行われません。反対にいずれのパネルも選択されていないとreturnが発動しないため、初期状態に戻す処理が行われます。

これでパネルをつけ替える処理もはずすだけの処理も実装完了しました。



ボタンを順番に押すパスワード

本作にはありませんがボタンを順番に押すパスワードも脱出ゲームではよく見かけます。結構簡単に実装できるのでちょっとそのコードも書いてみましょう。

public class Order : Password {
  private string answer;

  public void SetOrder(string text) {
    answer += text;
  }

  public void CheckAnswer(string right) {
    if(answer == right) {
      //パスワード解除
    }
  }
}

ボタンが押されるたびにanswer変数の後ろに文字を連結していって、CheckAnswer発動時にanswerの中身が正解と一致していれば解除するという動きです。連結する文字はボタンの中のテキストを使っても良いと思いますが、ボタンにテキストがついてない場合もあると思うので、そういう時は引数でn番目のボタンが押されたことが分かるようになっていれば問題ないでしょう。

ただしこれだけだとさっきのパネルの時と一緒で、一度間違えてしまうと最初からやり直す方法がないので詰んでしまいます。どうあがいてもクソ謎です。

なので何らかのタイミングでリセットする処理も入れる必要があります。不正解だった時とか、パスワード画面を開いた時なんかが良さそうですね。

public class Order : Password {
  public Dialog password;
  public GameObject detail;
  private string answer;
  private bool locked = true;

  public void OpenPassword() {
    if(locked) {
      answer = "";
      password.OpenDialog(detail);
    }
  }

  public void SetOrder(string text) {
    answer += text;
  }

  public void CheckAnswer(string right) {
    if(answer == right) {
      //パスワード解除
    } else {
      answer = "";
    }
  }
}

OpenPasswordはパスワード画面を開く関数です。画面を開く時に一緒にanswerの中身を空にすることで、画面を開くたびに一から入力できる状態になります。でも入力を間違えた時に画面を閉じずにやり直すこともあると思うので、間違えた時にもリセットすることで隙を生じぬ二段構えにできます。






これまで本作にないパターンも含めると6種類のパスワードを作りました。どれもそこまで複雑な処理を必要とせずに済むようなコードを書いたつもりなのですが……分かりづらいところがあれば遠慮なく訊いてもらえればと思います。

僕的にはどのパターンもあまり難しいことはやってないつもりですが、でも本シリーズで紹介しなかったパターンのパスワードもだいたいこれらの応用で実装できるんじゃないかなあという気はしています。もっと複雑な入力や判定を必要とするパスワードでも根本はそんなに変わらないと思うのよね。

まあ、もし「こんなパターンのパスワード考えてんだけどお前のコードの応用じゃどうにもならん。何かアイデア出せゴルァ」ってのがあればご一報ください。どういうパターンのパスワードでどういうコードを書いたら良いのか興味あるし、新しいパターンを知るのは僕としても良い勉強になりますからね。

何にせよこれでパスワード作りは終わりです。次回は音声オブジェクトを使って効果音をつけてみましょう。

それじゃあ次回もよろしくお願いしんたっくす。



本シリーズの記事の一覧はこちら
Unityで簡単な2D脱出ゲームを作ってウェブサイトで公開してみよう 〜エピローグ〜
 もしかしたら何か関連しているかも? 
 質問や感想などお気軽にコメントしてください