Unityでパチスロ風アプリを作ってみる

# はじめに

この記事は[リンク情報システム](https://www.lis.co.jp/)の2018年アドベントカレンダーのリレー記事です。
engineer.hanzomon のグループメンバによってリレーされます。
（リンク情報システムのFacebookは[こちら](https://www.facebook.com/lis.co.jp/)から）

クリスマスも差し迫ってきた年末、いかがお過ごしでしょうか。
22日目の記事は、@hunyuが担当いたします。よろしくお願いします。

<!--##################################################-->
# なぜこのお題にしたのか
<font color="red">**ワシぁ…勝ちたいんや…。**</font>
自分で勝てるスペック考えて作れば勝てる。なんて簡単なんだぁ…（気づき）

<!--##################################################-->
# もうちょい真面目な理由
以下の理由です。

- もともと、Unityに興味があったため。
- スロットは（私が悲しみを抱えた分だけ）詳しいので、それっぽいものであれば作りやすいと思ったため。

Unityについては、インストールして**1年くらい放置していた**し、いい勉強の機会になるかなと思い、挑戦してみた次第です。

<!--##################################################-->
# 開発環境
**Unity 2018.2.17f1**
放置していたものは古いバージョンだったので、適当な最新バージョンにしました。

<!--##################################################-->
# Unityについて軽くお勉強
開発するにあたり、諸々の使い方を独学にて勉強。
なるほど、**よくわからん。**

機能が豊富すぎて逆によくわからないのと、ゲーム業界の用語？が多い印象。

まあ、作っていけばなんとかなるか…。ということで、開発開始しました。

<!--##################################################-->
# スロットの機能や動作を整理
一般的なスロットに備わっている機能や動作を、ざっくりと洗い出すと以下のようなものがあります。
対応の難易度は、想像で勝手に降ってみました。
ついでにどこまで対応可能かなというのも考えてみました。（青字のものが今回の対応対象）

---
- **難易度：低**
    - <font color="blue">乱数の抽選処理</font>
    - <font color="blue">乱数をもとにした小役判定処理</font>
    - <font color="blue">べットおよび小役をもとにしたメダル枚数増減処理</font>
- **難易度：中**
    - <font color="blue">リール回転処理</font>
    - <font color="blue">リール停止処理（１リール）</font>
    - ウェイト処理
    - 小役カウンター実装
    - 状態保存機能（セーブ機能）の実装
    - 数段階の設定による小役判定処理の変更処理
    - 特定小役によるボーナス抽選処理の実装
    - ボーナスゲームの実装
    - 高確率の実装
- **難易度：高**
    - RT、ARTの実装
    - リール停止処理（３リール）
    - リーチ目の実装
    - ３リール時の押し順毎のリール停止目制御処理
    - ビタ押し処理
    - モードの実装
- **難易度：EX**
    - 癖になる音楽・SEの作成
    - カッコ可愛い画像・キャラの作成
    - 斬新で面白いゲームシステムの考案

---
…全てを実装するには、道のりは険しく、そして長く…。
（つーか音楽とか画像とかはもはや個人では厳しいのでは…）

急ぎでぱぱっとまとめたので、この程度しかないですが他にも（細かい話も含めて）沢山あると思います。

今回は期間が短いので、難しいものは無理と判断し、比較的簡単な部分だけ作ることにしました。

<!--##################################################-->
# 今回作るスロットの仕様
こんな感じで作ります。

- リール数は「１」とします。ライン数も「１」とします。
- べットと始動（以降スピンボタン）は１ボタンに集約します。ボタン押下によってリールが動作します。
- スピンボタン押下によって、0～999の1000個の乱数から1個を抽選します。
- 抽選結果の乱数から、小役を判定します。判定条件はLotteryCreator.csのGetKoyakuメソッドのコメント参照のこと。
- ストップボタン押下によって、リールを停止します。
- １ゲームあたり、メダルを３枚消費するものとします。（１リール１ラインなのにね）
- 小役成立時の配当は、「スロット用の画像作成」を参照のこと。

<!--##################################################-->
# スロット用の画像作成
スロットといえばやはりスロットの「図柄」ですね。
とりあえず、こいつがないと始まらない…ということで自前で作成しました。
今回は、以下の8種類用意しました。

<img width="100" alt="ブランク" src="https://qiita-image-store.s3.amazonaws.com/0/312786/2963849a-35fd-93b1-84cf-119938e85fe4.png">　ブランク。はずれ。きらい。たまに好き。
<img width="100" alt="リプレイ" src="https://qiita-image-store.s3.amazonaws.com/0/312786/d717998f-0fe8-b8ab-5f0e-55904542215d.png">　リプレイ。よく擬似ボーナスの的になる小役。払い出し3枚役扱い。
<img width="100" alt="チェリー" src="https://qiita-image-store.s3.amazonaws.com/0/312786/c68597f4-591d-c327-e065-1e0cd52c5f89.png">　チェリー。2つ股や3つ股のものが多い。4枚役。
<img width="100" alt="ベル" src="https://qiita-image-store.s3.amazonaws.com/0/312786/aecbb72d-4599-454d-01a8-62dc7e1a33c8.png">　ベル。たまにコインとか別のものだったりする。10枚役。
<img width="100" alt="スイカ" src="https://qiita-image-store.s3.amazonaws.com/0/312786/fe02d94d-30e1-37a9-e9ec-d704d3ce54f5.png">　スイカ。実機なら種まで見えます（謎の自慢）。15枚役。
<img width="100" alt="バー" src="https://qiita-image-store.s3.amazonaws.com/0/312786/ff21b894-3cb2-ab8c-f4eb-b171c2201deb.png">　バー（REG）。天井BAR単発を思い出せ。150枚役扱い。
<img width="100" alt="赤７" src="https://qiita-image-store.s3.amazonaws.com/0/312786/96d667d6-cd5e-3e88-c7ff-17c0bba85758.png">　赤７（BIG）。なぜか青や他の色に負けている事が多い。350枚役扱い。
<img width="100" alt="青７" src="https://qiita-image-store.s3.amazonaws.com/0/312786/8dce3597-43ba-341b-02ae-3ceb7ed2e0ae.png">　青７（BIG）。君は強く、そして美しい。711枚役扱い。

これらの画像を、縦並びにしたリールが↓。
<img width="30" alt="青７" src="https://qiita-image-store.s3.amazonaws.com/0/312786/b78f6dfc-3608-3eba-74f0-87759f10b4b2.png">

で、このリールに↓の画像をかぶせて…
<img width="200" alt="青７" src="https://qiita-image-store.s3.amazonaws.com/0/312786/ec8ff78f-f725-e60e-80df-73359ed1e19d.png">


真ん中の穴からぐるぐる回るリールをチラ見できるようにして、してスロット風に動かそう！…という魂胆です。


<!--##################################################-->
# 処理の実装
では、早速実装。
作成するスクリプトは以下の3つです。

- **ReelController**
    - リール制御用のスクリプト。リールの画像にアタッチする。
- **LotteryCreator**
    - 抽選結果生成用のスクリプト。スロットにおける心臓。
- **CanvasController**
    - 画面に表示するメダル枚数を管理するスクリプト。テキストエリアにアタッチする。

中身はこんな感じ。

```c#:ReelController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

using Assets.LotteryCreator;

// リール周りの動きをまとめたクラス
public class ReelController : MonoBehaviour
{
    // 共通変数
    private bool SpinButtonFlg;     // spinボタン用フラグ
    private bool StopButtonFlg;     // stopボタン用フラグ

    private float TargetPointA;     // 停止位置A
    private float TargetPointB;     // 停止位置B　※A以上B未満の位置でリール停止

    LotteryCreator lottery = new LotteryCreator();


    // スプライト生成時に動作する処理
    void Start()
    {
        SpinButtonFlg = false;
        StopButtonFlg = false;
        TargetPointA = 0f;
        TargetPointB = 0f;
    }

    // スプライト生成後、常に監視して動作し続ける処理
    void Update()
    {
        // stopボタンが押されたら（先にチェック）
        if (StopButtonFlg &&
            (transform.position.y <= TargetPointA && transform.position.y > TargetPointB)
           )
        {
            SpinButtonFlg = false;
            StopButtonFlg = false;

            // win枚数反映処理
            GameObject.Find("Canvas").GetComponent<CanvasController>().WinMedal();
        }

        // spinボタンが押されたら
        if (SpinButtonFlg)
        {
            // リール回転処理
            transform.Translate(0, -0.6425f, 0);

            // リール位置が一定位置を超えたら先頭に戻る
            if (transform.position.y < -10.25f)
            {
                transform.position = new Vector3(0, 10.2f, 0);
            }
        }

    }

    //　画面上に配置しているボタンを押したときの処理
    void OnGUI()
    {
        // spinボタン
        if (GUI.Button(new Rect(10, 320, 100, 30), "bet and spin"))
        {

            // bet枚数反映処理
            GameObject.Find("Canvas").GetComponent<CanvasController>().InsertMedal();

            // リール停止位置AとBを取得
            TargetPointA = lottery.LotteryKoyakuPoint();
            TargetPointB = TargetPointA - 0.6425f;

            SpinButtonFlg = true;

        }

        // stopボタン
        if (GUI.Button(new Rect(385, 320, 100, 30), "stop"))
        {
            StopButtonFlg = true;
        }
    }

}
```

```c#:LotteryCreator.cs
using UnityEngine;

namespace Assets.LotteryCreator
{
    // 抽選関連の処理をまとめたクラス
    public class LotteryCreator
    {

        // 共通変数
        private int MaxLottery;     // spinボタン用フラグ

        // 小役位置返却メソッド
        public float LotteryKoyakuPoint()
        {
            // 抽選
            var lottery = Lottery();
            
            // 抽選結果から小役を選択
            int koyaku = GetKoyaku(lottery);

            // 小役からwin枚数判別処理
            GameObject.Find("Canvas").GetComponent<CanvasController>().GetMedal(koyaku);

            // 抽選結果をもとに小役判定、位置をretrun
            return KoyakuPoint(koyaku);
        }

        // 抽選処理（ここの偏り一つで世界が変わる）
        private int Lottery()
        {
            var random = new System.Random();

            MaxLottery = 1000;

            // 0～999まで（MaxLottery個）の結果をreturn
            return random.Next(MaxLottery);

        }

        // 抽選結果から小役振り分け（ここ次第で勝ち負けが大きく変わる）
        // 【memo】
        // それぞれの小役に対応する番号と対応する抽選結果の範囲は以下の通り。
        // 0：ＢＲ　0～769　（約1.3分の1）
        // 1：リプ　770～894（8分の1）
        // 2：チェ　945～984（20分の1）
        // 3：ベル　945～984（25分の1）
        // 4：ｽｲｶ   985～992（125分の1）
        // 5：バー　993～996（250分の1）
        // 6：赤７　997～998（500分の1）
        // 7：青７　999     （1000分の1）
        private int GetKoyaku(int lottery)
        {
            if (lottery >= 0 && lottery <= 769) return 0;
            else if (lottery >= 770 && lottery <= 894) return 1;
            else if (lottery >= 895 && lottery <= 944) return 2;
            else if (lottery >= 945 && lottery <= 984) return 3;
            else if (lottery >= 985 && lottery <= 992) return 4;
            else if (lottery >= 993 && lottery <= 996) return 5;
            else if (lottery >= 997 && lottery <= 998) return 6;
            else if (lottery <= 999) return 7;
            else return 0;
        }

        // 抽選結果をもとに小役判別して小役位置を返すメソッド
        //
        // 【memo】
        // それぞれの小役に対応する番号と位置は以下の通り。
        // 0：ＢＲ　-9.50f（本来は-10.20f、リール周期の都合上、-9.50f）
        // 1：リプ　2.57
        // 2：チェ　7.71
        // 3：ベル　0.00
        // 4：ｽｲｶ 　-2.57
        // 5：バー　5.14
        // 6：赤７　-7.71
        // 7：青７　-5.14
        private float KoyakuPoint(int koyaku)
        {

            // リール位置返却
            switch (koyaku)
            {
                case 0: return -9.50f;
                case 1: return 2.57f;
                case 2: return 7.71f;
                case 3: return 0.00f;
                case 4: return -2.57f;
                case 5: return 5.14f;
                case 6: return -7.71f;
                case 7: return -5.14f;
                default: return -9.50f;
            }
        }

    }
}
```

```c#:ReelController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

// メダル枚数表示に関する処理をまとめたクラス
public class CanvasController : MonoBehaviour {

    int medalSwap = 0;      // Medal枚数退避エリア（一旦こっちにいれる）
    int medal = 0;          // Medal枚数退避エリア（表示するのはこっち）
    GameObject medalText;

    // 初期化
    void Start () {
        this.medalText = GameObject.Find("Medal");
    }

    // 常時動作
    void Update () {
        medalText.GetComponent<Text>().text = "MEDAL: " + medal.ToString("D1");
    }

    // bet枚数反映処理
    public void InsertMedal()
    {
        this.medalSwap -= 3;
        this.medal = this.medalSwap;
    }

    // 抽選結果をもとにwin枚数を返すメソッド
    //
    // 【memo】
    // それぞれの小役に対応する番号とwin枚数は以下の通り。
    // 0：ＢＲ　0
    // 1：リプ　3
    // 2：チェ　4
    // 3：ベル　10
    // 4：ｽｲｶ 　15
    // 5：バー　150
    // 6：赤７　350
    // 7：青７　711
    public void GetMedal(int koyaku)
    {
        switch (koyaku)
        {
            case 0:  this.medalSwap +=   0; break;
            case 1:  this.medalSwap +=   3; break;
            case 2:  this.medalSwap +=   4; break; 
            case 3:  this.medalSwap +=  10; break; 
            case 4:  this.medalSwap +=  15; break; 
            case 5:  this.medalSwap += 150; break; 
            case 6:  this.medalSwap += 350; break; 
            case 7:  this.medalSwap += 711; break; 
            default: this.medalSwap +=   0; break; 
        }
    }

    // medalSwapに入れたwin枚数含む現時点でのMedal枚数をmedalに反映する処理
    public void WinMedal()
    {
        this.medal = this.medalSwap;
    }
}
```

<!--##################################################-->
# 遊んでみる
できたらあとは“遊戯”っちまうだけ…！

実行！

![slot.gif](https://qiita-image-store.s3.amazonaws.com/0/312786/f25c81e8-784b-ce30-fc4d-9fe9252f2d46.gif)

おお！うごいた！リールが滑って小役をバシッと止めています！
ちなみに、gifだとわかりにくいけど、結構ヌルヌル動いています！いい感じ！

<!--##################################################-->
# 禁断の遊び
<font color=red>**では…始めようか…**</font>

```c#:LotteryCreator.cs
        // 抽選結果から小役振り分け（ここ次第で勝ち負けが大きく変わる）
        // 【memo】
        // それぞれの小役に対応する番号と対応する抽選結果の範囲は以下の通り。
        // 0：ＢＲ　0～769　（約1.3分の1）
        // 1：リプ　770～894（8分の1）
        // 2：チェ　945～984（20分の1）
        // 3：ベル　945～984（25分の1）
        // 4：ｽｲｶ   985～992（125分の1）
        // 5：バー　993～996（250分の1）
        // 6：赤７　997～998（500分の1）
        // 7：青７　999     （1000分の1）
        private int GetKoyaku(int lottery)
        {
//            if (lottery >= 0 && lottery <= 769) return 0;
//            else if (lottery >= 770 && lottery <= 894) return 1;
//            else if (lottery >= 895 && lottery <= 944) return 2;
//            else if (lottery >= 945 && lottery <= 984) return 3;
//            else if (lottery >= 985 && lottery <= 992) return 4;
//            else if (lottery >= 993 && lottery <= 996) return 5;
//            else if (lottery >= 997 && lottery <= 998) return 6;
//            else if (lottery <= 999) return 7;
            if      (lottery >=   0 && lottery <= 249) return 1;
            else if (lottery >= 250 && lottery <= 499) return 5;
            else if (lottery >= 500 && lottery <= 749) return 6;
            else if (lottery >= 750 && lottery <= 999) return 7;
            else return 0;
        }
```

実行！！！！！！！

![manmai.gif](https://qiita-image-store.s3.amazonaws.com/0/312786/50e43e28-191f-2838-0fdc-4a49aa6abcd6.gif)
クッソチョロすぎｗｗｗ
40秒で万枚ｗｗｗｗｗｗ
時給90万枚ｗｗｗｗｗｗｗｗｗｗ

**虚しい**



<!--##################################################-->
# まとめ
## スロットに関して
作った本人が言うのもアレですが、**このスロット、面白くない**です。
理由は明確。

- **音がない**
- **絵が汚い**
- **目押し要素がない**

やっぱこの3つは大切ですね。
今度ちゃんと作る機会があったらやっぱこれらはキチンと作らないといけないな、と感じました。

## Unityに関して
Unityからは、**無限の可能性**を感じました。
…単純に私が知らない機能がもっといっぱいある！って話なのかもしれませんが。
作り手の知識や、シナリオ次第で本当にどんなものでもできてしまう底の深さを感じました。

何より、とりあえずやってみようで本当に出来ちゃうあたり、**包容力も抜群**ですね。
私が適当に考えた仕様でスロットができたくらいですし。すごいツール。

もっと勉強したくなりました。

<!--##################################################-->
# さいごに
思ったよりさっくり終わった感はありますが、気づけば深夜までやっていたりしていました。
やっぱ開発が好きなんだな、と改めて実感いたしました。
誘ってくれた@dgkzさん、ありがとうございました。

明日の担当は、@i-ohbaさんです！お楽しみに！