ランサーズ(Lancers)エンジニアブログ > iOS > uGUIで簡単!Unityクイズアプリで社内コミュニケーションの活性化

uGUIで簡単!Unityクイズアプリで社内コミュニケーションの活性化

tsuyoshi|2015年05月28日
iOS

みなさんはじめまして。2015年3月後半からジョインしました、ランサーズWEBエンジニアのTsuyoshiです。ランサーズは近頃成長フェーズなので、この数ヶ月で多くのメンバーがジョインしました。私もそのうちの一人です。ベンチャーにおける成長フェーズでは、増えた社員同士のコミュニケーションやバックグラウンドの共有などが上手くいかないのが、ベンチャーあるあるだと認識してます。

今回は、そういったあるあるが起こる前に、新メンバーと既存メンバーがシームレスにコミュニケーションを取れる場を提供するため、みんなのことを気軽に知れる社内クイズアプリをUnityで作ってみました。

できたゲームはこんな感じです。(プライバシー保護のため、コンテンツの内容は改変しております)

ゲーム画面

ゲーム画面

この記事では、Unityを使った基本的なゲームの作り方などをご紹介し、コミュニケーション不足に悩まされるベンチャー企業の方々の手助けになればと思います。

(ちなみに今回は、チャレンジドリブンなランサーズの文化を踏襲すべく、WEBアプリではなく、あえて自分も初めて使うUnityのuGUI機能に挑戦してみました)

*ソースコードはページの一番下にあります。

 

 

Unityのダウンロードとプロジェクトの作成

まずはUnityをダウンロード。(ここからのレベルです)

http://japan.unity3d.com/unity/download/

このプログでご紹介するバージョンは5.0.1です。
会員登録などを済ませて、早速起動します。(無料版)

プロジェクトを作成します。「2D」を選び「Create Project」しましょう。

CreateUnityProject

CreateUnityProject

プロジェクトが作成できたら、レイアウトを「2by3」にして、「Window」→「console」で、コンソール画面を表示すると、後々作業しやすいです。本ブログでも、そのレイアウトで進めて参ります。
画面の見方を下記の図に簡単に示します。

WindowDetail

画面の説明

 

 

スタート画面を作る

今回作成するアプリは、下記のようなシンプルな画面遷移をするアプリです。

Quizゲームフロー

Quizゲームフロー

まずは、ゲームのスタート画面から作りましょう。
スタート画面にはLancersのロゴを入れたいと思います。
まずはおもむろに、「Scene」タブのなかに、入れたい画像をドラッグ&ドロップしましょう。

Lancers_create_image

画像ファイルをUnityにドラッグ&ドロップ

JPEGの画像を入れただけで左下のゲーム画面にもロゴが表示されました。
プロジェクトを作成した際に「2D」を選択しましたが、Unityの2Dモードでは、
画像は「Sprite」という単位で扱われることを、頭のどこかに置いておいてください。

 

 

スタートボタンを設置する

次に、Unity4.6以降から実装されたuGUIを使って、スタートボタンを作ります。
「GameObject」→「UI」→「Button」を選択します。

GameObjectUIButton

UI Buttonを設置

「Hirrarchy」タブに「Canvas」→「Button」というオブジェクトが現れたかと思います。基本的にUnityのuGUIの機能では、すべてのUI要素はCanvasの子要素として扱われます。なので、ボタンを増やすと、自動的にCanvasの下に入ってきます。

また、「Button」だけでなく「EventSystem」というオブジェクトも一緒についてきますが、このEventSystemを通して、ボタンとマウスの当たり判定などが計算される仕組みです。

Sceneに対して大きすぎる UIボタン

Sceneに対して大きすぎるUI

Canvasを作成した際、上図のように最初に入れた画像よりも異常に大きいボタン(もしくは小さいボタン)がScene画面に表示される場合は、「Canvas」のInspectorから「ScreenSpace」を「Main Camera」に合わせてあげるとカメラとUIが1:1となって上手くいきます。

カメラとUIを1対1の大きさに

カメラとUIを1対1の大きさに

作成したボタンの名前は、Canvas→Button→Text から「GameStart」のような文字列に変えることができます。

 

 

ButtumNameChange

ボタンの表示テキストを変更する

 

 

ゲーム画面の遷移について

「GameStart」ボタンもできたので、早速画面を切り替えてみましょう。
現在のゲーム画面を保存し、新しいゲーム画面を追加します。
現在のゲーム画面を「File」→「Save Scene」より「Title」という名前を付けて保存します。

SaveUnityScene

画面をSceneとして保存する

保存できたら「File」→「New Scene」より、新しいSceneを作成します。

すると、まっさらなゲーム画面ができるので、一旦これも保存しましょう。

ゲームスタート後に遷移する画面を想定しているので、クイズアプリらしく、出題画面にしたいので、「File」→「Save Scene」より「Quiz」という名前で保存してください。

すると、Unityのマークがついた「Title」「Quiz」というファイルができます。

SaveSceneFilese

シーンファイルの作成

この「Title」や、「Quiz」をダブルクリックしてあげると、それぞれのシーンを編集していくことができます。一旦「Title」→「Quiz」の遷移を作成するため、

「File」→「Build Settings」を開いて、「Title」と「Quiz」をドラッグ&ドロップして追加してください。この作業を行うことで、シーンからシーンの移動が可能になります。

QuizStartBuildSetting

BuildSettingsへのシーン追加

※これだけでは遷移しないので、次はボタンがクリックされたら遷移するロジックを書きます。

 

uGUIでの画面遷移:onClickイベント

「GameStart」をクリックすると「Quiz」画面に遷移するロジックを作りたいと思います。ロジックの作成は、スクリプトを書くことで実現できます。

「Assets」→「Create」→「C# Script」でファイル名を「GameStart」にします。

AddScriptOnClick

C#スクリプトを追加

作成したスクリプトに以下のコードを書きます。

using UnityEngine;
using System.Collections;

public class GameStart : MonoBehaviour {

    public void  NextScene(){
        //今いるシーンがTitleという名前であれば、Quizという名前のシーンに移動する
        if (Application.loadedLevelName == "Title") {
            Application.LoadLevel ("Quiz");
        }
    }
}

Application.loadedLevelNameというのが、現在のScene名に対応しているので、Titile画面であれば、Quiz画面に遷移するという単純なロジックです。

ロジックができたら、ボタンにスクリプトをアタッチしましょう。

ここからようやくuGUIの登場です。

スクリプトをボタンに設置

スクリプトをボタンに設置

次に、Canvas→Buttonの、On Click()イベントに、ボタンをリックした時に起こる動作を指定します。

  1. Canvas→Button 「On Click」「+」ボタンでイベントを追加。
  2. Buttonのゲームオブジェクト自身をドラッグ&ドロップで割り当てる。
  3. Click時に実行する関数を、先ほど作ったメソッド「NextScene」に設定。
CallScriptOnclick

クリックイベントの設定

ここまで出来ると、Title画面からQuiz画面へ遷移することができます。

実行画面で確認してみましょう。

無事遷移できましたか?Xcodeのストーリーボードや、AndroidStudioではこの辺りの処理は一瞬でできそうですが、Unityをつかってアプリの画面遷移を作るとこんな感じになるかと思います。

Unity自体が、こういったアプリを作ることに向いていないのもあるかもしれませんが、逆にUnityなら面倒なことをすれば、こんなこともできちゃう。とポジティブに捉えましょう。

 

 

クイズ画面の作成

スタート画面から遷移できたので、次は、実際のクイズ問題画面を作っていきます。

今回は、画像のお題に対して4択で答えるシンプルなアプリにするので

  • 画像
  • 問題文
  • 回答1、回答2、回答3、回答4

を画面に配置していきたいと思います。早速作っていきたいところですが、Quizのシーンを開くと、先ほどと同じように、Canvasを置いて〜、とやる必要があるので、同じ設定を何度もするのは面倒です。同じ機能を実装する際はPrefab機能を使います。

 

Prefab化して、同じ画面を呼び出すオブジェクトを作る。

UnityにはPrefabという機能があり、同じ効果を持つオブジェクトを簡単に複製できます。今回は、Canvasを映しているMainCameraをPrefab化して、シーンが移動しても、同じ画面をすぐに作れるようにしましょう。

Hierarchy タブにて、MainCameraの中にCanvasオブジェクトをドラッグ&ドロップ。

MainCameraObjectをProjectタブのAssetsにドラッグ&ドロップ。

ゲームオブジェクトのプレハブ化

ゲームオブジェクトのプレハブ化

すると、図のような青いキューブができます。

これがPrefabです。シーンを切り替えても、リソースとしてフォルダに残っているので、各シーンで扱えます。(ドラッグ&ドロップでHierarchyタブに持っていく)

では、Quiz画面に行って、Prefabを使ってみましょう。

新しいシーンにはデフォルトでMainCameraがあるので、一旦削除します。

先ほど作った、PrefabをProjectタブからHierarchyタブにドラッグ&ドロップします。(デフォルトのMainCameraを削除しておくことを忘れないでください。また、prefabをコピーして作るとEventSystemが作成されないので、EventSystemも「GameObject」→「UI」→「EvetntSystem」から生成しておきましょう)

Prefabload

プレハブをSceneに追加する

 

 

UIを配置する

今までやってきたことを組み合わせて、「画像」「ボタン」「 テキスト」を配置します。

  • 画像は任意の画像をドラッグ&ドロップ
  • ボタンは GameObject→UI→Buttonから
  • テキストも GameObject→UI→Text

またCanvasの名前を「Quiz」に、「Button」の名前を「AnsButton」に書き換え、それぞれの要素に固有の名前をつけてあげましょう。そうすることでスクリプトから固有の名前の要素にアクセスできます。

変更した各オブジェクトの名前

変更した各オブジェクトの名前

 

 

問題文と回答文を挿入

問題文と、回答選択ボタンができました。実際に問題文などを読み込ませましょう。問題文や回答文は動的に変化させたいので、オブジェクトのテキストにスクリプトからアクセスして変化させてみます。

新しくAssets→Create→C#Scriptから、「QuizMgr」というスクリプトを作成します。

using UnityEngine;
using System.Collections;
using UnityEngine.UI;//UI オブジェクトを扱う時は必須
public class QuizMgr : MonoBehaviour {

     //アタッチしたオブジェクトが呼ばれた時に実行される。
    void Start () {
        QuestionLabelSet ();
    }

    private void QuestionLabelSet(){
        //特定の名前のオブジェクトを検索してアクセス
        Text qLabel = GameObject.Find("Quiz/QLabel").GetComponentInChildren<Text> ();
        //データをセットすることで、既存情報を上書きできる
        qLabel.text = "ランサーズ君は何歳?";
    }
}

Findを使えば、UI部品につけた名前をキーに検索して、GameObjectにアクセスできます。上記場合「Quiz配下にあるQLabelの中にあるTEXT」の情報を取得するという意味です。

スクリプトができたら、MainCamera、もしくは空のオブジェクトを作って、そこにアタッチします。(シーンを開いた時に常にあるオブジェクトに適応)

ScriptAttach

スクリプトをオブジェクトにアタッチ

アタッチできたら実行してみましょう。

 

QLabelの文字が変化します。

LabelChange

テキストの変更

同様に、回答(AnsButton)のラベルも変更しましょう

 

QuizMgrを以下のように書き換えます。

using UnityEngine;
using System.Collections;
using UnityEngine.UI;//UI オブジェクトを扱う時は必須
public class QuizMgr : MonoBehaviour {

    //アタッチしたオブジェクトが呼ばれた時に実行される。
    void Start () {
        QuestionLabelSet ();
        AnswerLabelSet ();
    }

    private void QuestionLabelSet(){
        //特定の名前のオブジェクトを検索してアクセス
        Text qLabel = GameObject.Find("Quiz/QLabel").GetComponentInChildren<Text> ();
        //データをセットすることで、既存情報を上書きできる
        qLabel.text = "ランサーズ君は何歳?";
    }

    private void  AnswerLabelSet(){
        //回答文面の作成
        string[] array = new string[]{"10歳","6歳","青二才","7歳"};
        //ボタンが4つあるのでそれぞれ代入
        for (int i=1; i<=4 ; i++){
            Text ansLabel = GameObject.Find("Quiz/AnsButton" + i).GetComponentInChildren<Text> ();
            ansLabel.text = array[i-1];
        }
    }

}
問題と解答テキストの挿入

問題と解答テキストの挿入

実行すると上記のような画面が表示されるかと思います。
クイズっぽくなってきましたね。
※データベースの扱いなどは、今回は説明しません。
次は、回答をクリックした時のイベントを作ります。

 

正誤判別をする

ユーザーが選択した選択肢の正誤を判別する処理を作ります。
新しく「Judgde」というスクリプトを書いてそれぞれのボタンに割り当てます。ボタンへの割り当ては「GameStart」ボタンのときに説明したuGUIを使いましょう。

using UnityEngine;
using System.Collections;
using UnityEngine.UI;

public class Judge : MonoBehaviour {

    //選択したボタンのテキストラベルと正解のテキストを比較して正誤を判定
    public void JudgeAnswer(){
        //正解のデータをテキストでセットする
        string answerText = "7歳";
        //選択したボタンのテキストラベルを取得する
        Text selectedBtn = this.GetComponentInChildren<Text> ();

        if (selectedBtn.text == "7歳") {
            Debug.Log("正解");
        } else {
            Debug.Log("不正解");
        }
    }
}

ボタンが押されたら、押された値によって、コンソール画面に、文字列を表示する単純なスクリプトです。

「Window」→「Console」でコンソール画面を開いた状態で、実行してみましょう。

ボタンをクリックするとConsole画面に、選択結果が反映されるかと思います。
*ボタンをクリックしても何も出ない場合は
・「EventSystem」オブジェクトがあるか?
なければ、「GameObject」→「UI」→「EventSystem」で作成
・スクリプトがきちんと割り当てられているか?
などを確認してみてください。

ChoiceAnswer

解答を選ぶ

 

 

 正誤判定画面へ遷移させる

クイズゲームなので、正解の回答を選択したら「正解!」誤った回答を選択したら「不正解!」と、表示させる画面を作ります。

「File」→「New Scene」で新しい画面を作ります。

シーンの名前は「Result」などの名前で保存してください。

Quizのシーンと同様に、カメラオブジェクトなどを設置します。今回は「◯」の画像を用意し、Imageとして配置しました。

JudgeImage

正誤画面

Quiz画面からこのResultシーンに移動するので、

「File」→「BuildSetting」から、新しく作った「Result」ファイルを追加します。

Judgeのスクリプトを編集し、Quizシーンの回答ボタンが押されたら、Resultシーンへ移動するようにします。

        if (selectedBtn.text == "7歳") {
            Debug.Log("正解");
            Application.LoadLevel ("Result");
        } else {
            Debug.Log("不正解");
            Application.LoadLevel ("Result");
        }

Quizシーンに戻って実行し、回答を選択すると、先ほど作った画面に移動できるかと思います。

ただ、今のままだと、不正解を押しても、「◯」が表示されるので、

正誤によって表示を切り替えてみます。

 

スクリプト間でのデータの受け渡し

状態によって表示を切り替えるために、スクリプト間でデータの受け渡しを行います。

データの受け渡し方法は色々ありますが、今回はグローバル変数を関数からセットする方法を用います。

Resultシーンにて 「Assets」→「Create」→「C#Script」で「ResultMgr」というスクリプトを作成します。

using UnityEngine;
using System.Collections;
using UnityEngine.UI;

public class ResultMgr : MonoBehaviour {
    //他のスクリプトからも参照可能な変数宣言
    public static string g_judgeData;

    void Start () {
        Debug.Log (g_judgeData);
    }

    //他のスクリプトからも参照可能な関数宣言
    public static  void SetJudgeData(string judgeData){
        g_judgeData = judgeData;
    }
}

public staticという宣言で、他のスクリプトからも関数などが読み込めるようになります。上記ではg_JudgeDataというグローバルな変数に値をセットするため、SetJudgeDataという他のスクリプトからも読み込める関数を用意しました。

回答選択 → 正誤判定 → 正誤データ受け渡し

としたいので、「Judge」スクリプトから、SetJudgeDataを呼び出し、g_JudgeDataに正誤データを受け渡してみます。

        if (selectedBtn.text == "7歳") {
            //選択したデータをグローバル変数に保存
            ResultMgr.SetJudgeData ("正解");
            Application.LoadLevel ("Result");
        } else {
            //選択したデータをグローバル変数に保存
            ResultMgr.SetJudgeData ("不正解");
            Application.LoadLevel ("Result");
        }

ResultMgr.SetJudgeData(string)という記述で、グローバル関数を呼び出し、値を引き渡せます。Quizシーンから遷移させてみます。

RunResult

実行結果

 

 

状態によって表示を切り替える

正解、不正解の正誤情報を送れるようになったので、表示を切り替えてみたいと思います。

まずは、切り替える画像を用意します。(マルの画像と、バツの画像)

画像の置き場は、Assetsフォルダの下に、新規で「Resources」フォルダを作り、そこに配置します。すでに配置してるマルの画像も、こちらに移しておきます。

Unityでは、Resourcesのように、Assetsフォルダの下につけるフォルダの名前が決まっているものがあります。(本来はScriptsやScenesなどのフォルダにまとめるのがルールです。※今回は動作第一優先)

Resourcesフォルダに置いたオブジェクトなどはResources.Load<T>()でスクリプトから呼び出すことができます。

なので、画像はResourcesフォルダ配下に保存します。

ImageAtResouces

Resourcesフォルダ配下に保存

ResultMgrのスクリプトのStart関数を以下のように書き換えます

    void Start () {
        //現在描画している画像を取得
        SpriteRenderer judgeImage = GameObject.Find("JudgeUI/JudgeImage").GetComponent<SpriteRenderer>();
        //Resourcesから指定した名前の画像データをロード
        Sprite loadingImage = Resources.Load<Sprite>("batsu");
        //画像を置換
        judgeImage.sprite = loadingImage;
    }

最初に置いてあった画像を取得し、新しい画像に書き換えます。
スクリプトを実行すると、以下のように、◯だった画像が×に変わります。
うまく切り替わらない場合は
・型を確認する。
Sprite、Image、SpriteRendererのようなGameObjectの型があるので注意。
・GameObject.Find()の中身が正しいか確認する
Hierarchy画面の文字列と合致している必要があります。

ExecuteGame

画像をスクリプトから切り替える

これで表示の切り替えができたので、選択した回答によって表示を切り替えたいと思います。
ResultMgrのStart関数をを以下のように書き換えます。

    //他のスクリプトからも参照可能な変数宣言
    public static string g_judgeData;

    void Start () {
        //デフォルトは正解、不正解なら画像と文言を切り替える
        if (g_judgeData == "不正解") {
            //現在描画している画像を取得
            SpriteRenderer judgeImage = GameObject.Find ("JudgeUI/JudgeImage").GetComponent<SpriteRenderer> ();
            //Resourcesから指定した名前の画像データをロード
            Sprite loadingImage = Resources.Load<Sprite> ("batsu");
            //画像を置換
            judgeImage.sprite = loadingImage;
            //表示テキストを取得して置換
            Text judgeLabel =  GameObject.Find("JudgeUI/JudgeLabel").GetComponent<Text>();
            judgeLabel.text = "不正解";
        }
    }

デフォルトの状態は正解とし、不正解の選択をした場合のみ、画像を切り替えます。
Quiz画面でゲームを実行すると、回答によって、表示が切り替わったかと思います。
あとは、Nextボタンに次の問題へいくロジックを書いてみます。画面遷移機能を持っているGameStart関数を書き換えて、ResultシーンならばQuizシーンに移動するように、スクリプトを書き加えます。

using UnityEngine;
using System.Collections;

public class GameStart : MonoBehaviour {

    public void  NextScene(){

        if (Application.loadedLevelName == "Title") {
            Application.LoadLevel ("Quiz");
        }
    }

    public void NextQuiz(){

        if (Application.loadedLevelName == "Result") {
            Application.LoadLevel ("Quiz");
        }
    }
}

スクリプトが書けたら、uGUIの機能を使って、GameStartスクリプトをNextボタンに付け加えて、ボタンイベントを登録しましょう。

NextScene

次のシーンへ移動させるスクリプトをアタッチ

これで、QuizシーンとResultシーンを行き来できるようになりました。

移動できない場合は、

  • Build Settingsに指定したシーンが登録されているか
  • UIのEventSystemがHierarchyにあるか
  • Buttonにスクリプトがコンポーネントとして入っているか

などを確認してみてください。

 

よりゲームらしくする

最後に、よりゲームらしくするために、得点機能をつけて、スコアを表示してみましょう。

ResultMgrのスクリプトに、正解だった場合、得点を加えるスクリプトを追記します。

    public static int g_scoreData;

        if (g_judgeData == "不正解") {
//..処理
} else if (g_judgeData == "正解") {
            //正解であればScoreを足す
            g_scoreData++;
        }
    //グローバルに宣言したスコアを他のスクリプトから読み込む
    public static int GetScoreData(){
        return g_scoreData;
    }
    //グローバルに宣言したスコアを他のスクリプトから書き込む
    public static int SetScoreData(int scoreData){
        g_scoreData = scoreData;
        return g_scoreData;
    }

<i>//グローバルに宣言したスコアを他のスクリプトから読み込む</i>

public static int GetScoreData(){

return g_scoreData;

}

これにより、g_scoreDataという変数に得点が蓄えられます。
スコアを最終的に表示するために、「File」→「NewScene」より、新しく「Score」というシーンを作成し、保存します。
保存したら、Build Settingに登録し、他のシーンから画面遷移できるようにしましょう。

ScoreScene

スコアSceneの作成

Scoreというスクリプトを書いてMainCameraにアタッチします。

using UnityEngine;
using System.Collections;
using UnityEngine.UI;

public class Score : MonoBehaviour {

    // Use this for initialization
    void Start () {
        //スコア表示用のゲームオブジェクトを取得
        Text scoreLabel = GameObject.Find("Canvas/Score").GetComponent<Text>();
        scoreLabel.color = Color.red;
        //グローバルに宣言したスコアをResultMgrのスクリプトから読み込む
        int Score = ResultMgr.GetScoreData ();
        scoreLabel.text = Score.ToString() + " 点" ;
    }
}

実行すると、指定したゲームオブジェクトが動的に変わるようになります。

ExecuteScoreScene

スコアSceneの実行結果

最後にGameStartスクリプトにて、3回問題を繰り返したら、Score画面に遷移するように、変更します。

public static int qCount;

public void NextQuiz(){

        if (Application.loadedLevelName == "Result") {

            if(qCount < 2){
                qCount++;
                Application.LoadLevel ("Quiz");
            }else{
                qCount = 0;
                Application.LoadLevel ("Score");
            }
        }
    }

    public void  BackToTitle(){

        if (Application.loadedLevelName == "Score") {
            ResultMgr.SetScoreData(0);
            Application.LoadLevel ("Title");
        }
    }

これで一通り、ゲームの完成です。

実際に動かしてみましょう。

ゲーム画面

ゲーム画面

*今回は説明するために流れをわかりやすくして書くことを心がけたため、変数の初期化場所などがスマートではありませんが、一通り作った後に、いろいろ試して書き換えていただければ幸いです。また、クイズの問題を変更するデータベースなどの作り方は、長くなりそうなので、またの機会にご説明できればと思います。

 

実際に会社のメンバーに使ってもらう

*実際に使ってもらったものは、社員のメンバーそれぞれの名前のクイズや、趣味などを問題として登録したものを使ってもらいました。

Aさん「面白いです。」
Mさん「なにこれすごーい。問題が面白い!」
Tさん「コンテンツ大事ですね。みんなで問題を編集したり追加できるようにしたい」
Hさん「だったらUnityじゃなくて、WEBアプリで良いのではないでしょうか?」

・・・・・

 

今後の課題

まだまだ、実装途中なので、コミュニケーションに関する結果はまだ出ていませんが、

・クイズの問題を社内メンバーで自由に作れるような機能を実装し、よりコミュニケーションの円滑化を図る。
・Unity以外にもWEBアプリで実装してみる。

などを視野に入れて、今後も取り組んでいきたいと思います。

 

まとめ

  • uGUIでぽちぽちしながらクイズゲームを作れる。
  • 画面遷移が少しとっつきにくい、慣れればいろんなパターンのゲームを作成可能
  • 冷静に社内コミュニケーションを活性化させるなら、WEBアプリでOK
  • 問題文や画像のコンテンツがとにかく大事

今回作成したファイルはこちらにアップロードしてあります。
ランサーズクイズアプリ

もしこの記事のはてぶ100ぐらい行ったら、データベース的な説明や、オンライン上でメンバーが問題を編集できる機能のつけ方を説明する新しい記事書きます。

これからも挑戦は続きます。

ランサーズでは、こういった楽しい開発を一緒にしてくださる、素直でポジティブなエンジニアを募集しております。ぜひ一緒に開発したいと思った方は、以下のページよりお気軽にお声がけください。