C#
Unity
クイズ

クイズゲームを作ろう!

クイズゲームを作ろう!

今回は、3択クイズゲームを作っていきます!

目次

ステップ① スプレッドシート(CSVスクリプト)でクイズの作成
ステップ② クイズ用のクラスの作成
ステップ③ クイズの表示
ステップ④ 当たり判定
ステップ⑤ 正誤判定
ステップ⑥ 選択肢をランダムに
ステップ⑦ 複数問題の出題
おまけ   複数ステージの実装

ステップ① スプレッドシートでクイズを作成しよう

CSVファイルを用意しよう!

まず、Googleドライブから新しいスプレッドシートを作成します。






















いちばん左のセルから順に問題文、正答、誤答1、誤答2を書き込んでいきましょう。
最後「;(セミコロン)」を入力するのを忘れずに!
保存する時は、【ファイル】→【形式を指定してダウンロード】→【カンマ区切りの値
    (.csv、現在のシート)】でCSVファイルとして自分のPCに保存します。
その際、ファイルの名前を「QuizForStage1」としておきましょう。

UnityプロジェクトにCSVファイルを入れよう!

Unityプロジェクトに戻って、Projectビューの[Create]から新しいフォルダを作成し、
「Resources」という名前にします。
さらに、Resourcesフォルダの中に「CSV」という名前のフォルダを作成し、自分のPC
のファイル/ファインダーから保存したCSVファイルをぽいっとドラッグ&ドロップしま
す。
これで、作問の準備は完了です!   

ステップ② クイズ用のクラスを作ろう!

クイズを入力するためのファイルはできたので、次はUnity上でCSVファイルのデータを使えるようにする準備をしていきます。
Unityプロジェクトを開いて、Projectビューの[Create]から新しいスクリプトを作ります。
スクリプトの名前を「QuizManager」にし、以下のようにコードを書いていきます

これで、クイズクラスの完成です。

ステップ③ クイズを表示しよう!

クイズの準備が一通りできました!
今度はUnityでクイズを1問だけ表示させてみましょう。

まず、Unityプロジェクトを開いて以下3種類を↓の画像のように配置していきましょう。
・問題文を表示するためのテキスト
・選択肢を表示するためのテキスト ×3つ
・選択肢ボタンがわりになるキューブ ×3つ
(キューブは、上から名前を「AnswerWallTop」、「AnswerWallMiddle」、「AnswerWallBottom」としておきます。)

テキスト・オブジェクトの配置が終わったら、次は「CsvManager」というスクリプトを用意するのですが、内容はコピペでOKです(注1)。
Projectビューの中にドラッグ&ドロップで入れておきます。

次に、新しいスクリプトを作成し、名前を「GameManagerScript」にします。
スクリプトを開いて↓のようにコードを書いていきます。

最後にスクリプトで宣言した変数とテキストとを紐付けし、実行してクイズが表示されればOKです。

ステップ④ 当たり判定をしよう!

今回は、”Ray”というコードを使って、タップしたキューブに当たり判定を設定していきます。

まず、「AspectRatioManager」という名前のスクリプトを作り内容をコピぺして(注2)、Projectビューの中にドラッグ&ドロップで入れておきます。

Rayはオブジェクトのタグで当たりを検知するので、選択肢のキューブ3つに「Answer」という名前のタグを作り、タグ付けします。

次に、GameManagerScriptのvoid Update関数内に当たり判定のコードを書いていきます↓

Unityプロジェクトに戻り、実行してみましょう!

ステップ⑤ 正誤判定をしよう!

押した選択肢が反応してくれるようになりました。
今、AnswerWallTopに正答が反映されるようになっています。
そこで、今度は上記の選択肢をタッチすると「正解」とコンソールに表示させてみましょう。

正誤判定をするために、以下のようにコードを追記していきます。

・「answerString」というstring型の変数を宣言(void Start関数の前)
・65行目のanswerStringに正答を保存
・75〜81行目の"AnswerSelected"関数

最後に、各選択肢にRayが当たった時の処理のところで、AnswerSelected関数を呼び出します。

ステップ⑥ 選択肢をランダムにしよう!

今の状態では、一番上の選択肢が正解になってしまっています。今度は、正答をランダムないちに表示する処理を行います。

はじめに、選択肢の表示をランダムにするための「ListExtension」スクリプトを作成し(注3)、UnityプロジェクトのProjectビューに入れておきましょう。

次に、GameMnager上で選択肢テキストを以下のようにシャッフルします
これで選択肢のテキストをランダムに表示できます。

ステップ⑦ 複数の問題を出題しよう!

最後に正誤判定をした後に、次の問題をランダムに出題する、という実装を行います。
GameMangerScriptを開いて、以下のようにコードを加筆・修正して行ってください。

コードの変更については、以下の4点を確認しましょう。

・CreateQuestion関数で、quizListがfor文になっているか
・「currentQuizIndex」変数が宣言されているか
・ShowQuestion関数の、quizList[0]がquizList[i]に変わっているか
・currentQuizIndexのランダム設定ができているか・ShowQuestion関数の引数がcurrentQuizIndexになっているか

これで、クイズゲームの基本システムができました!!

おまけ 複数のステージを実装しよう!

おまけですが、複数ステージを作る時、ステージの数だけシーンを作る必要はありません。

ステップ①を参考に、もう一つのステージ用のCSVファイルを作成し、名前を「QuizForStage2」として作成します。

次に、新しくProjectビューの[Create]からステージ選択用のシーンを作ります。
また、新しいステージにボタンを2つ追加し、片方を押下するとCSVファイルのQuizForStage1が読み込まれ、もう片方を押下するとQuizForStage2が読み込まれるようにしていきます。

シーンができたら、今度は新しいスクリプトを作成し、「MenuManagerScript」と名前をつけ、以下のようにボタンをクリックした時の関数を2つ作ります。

ボタンと関数を紐付けられたら、GameManagerScriptに戻り、以下のようにコードを書き換えます。

以上、複数ステージの実装でした!

コピペファイル一覧

注1. CsvManager

using UnityEngine;
using System.Collections;
using System.IO;
using System.Collections.Generic;
using System.Text;


public class CsvManager
{
 ///


 /// Resourcesフォルダ内のcsvファイルを読み込み、string型2次元配列で返す。
 /// example) CsvManager.ReadTextFile("Commands/BasicCommands.csv")
 /// 
 public static string ReadTextFile (string dataPath)
 {
 string data;
 string textData = OpenTextFile (GetPath () + dataPath);
 if (textData != "ERROR") {
 data = textData;
 } else {
 data = "";
 }

 if (string.IsNullOrEmpty (data)) {
 return null;
 }
 return data;
 }

 public static string[,] ReadCsvFile (string dataPath)
 {
 string data;
 string textData = OpenTextFile (GetPath () + dataPath);
 if (textData != "ERROR") {
 data = textData;
 } else {
 TextAsset textAsset = (TextAsset)Resources.Load (dataPath.Split ('.') [0]);
 data = textAsset.ToString ();
 }

 if (data == null) {
 return null;
 }
 string[] rows = data.Replace ("\ r\n", "\ n").Split ('\ n');
 int dataRows = rows.Length;
 int dataCols = rows [0].Split ("," [0]).Length;
 string[,] csvData = new string[dataRows, dataCols];
 for (int i = 0; i < csvData.GetLength (0); i++) {
 for (int j = 0; j < csvData.GetLength (1); j++) {
 string[] value = rows [i].Split ("," [0]);
 csvData [i, j] = value [j];
 }
 }
 return csvData;
 }


 /// 
 /// Resourcesフォルダ内に、ファイルを書き込む。
 /// 例) csvManager.WriteData("data.csv", dataArray);
 /// 
 public static void WriteData (string dataPath, string[,] newData)
 {
 Debug.Log("[CsvManager.WriteData(string dataPath, string[,] newData)] called");
 string stringData = "";
 for (int i = 0; i < newData.GetLength (0); i++) {
 for (int j = 0; j < newData.GetLength (1); j++) {
 if (j < newData.GetLength (1) - 1) {
 stringData += newData [i, j] + ",";
 } else if (j == newData.GetLength (1) - 1 && i < newData.GetLength (0) - 1) {
 stringData += newData [i, j] + "\ n";
 } else {
 stringData += newData [i, j];
 }
 }
 }

 string[] directries = dataPath.Split ('/');
 for (int i = 0; i < directries.Length-1; i++) {
 string tmpPath = "";
 for (int j = 0; j < i + 1; j++) {
 tmpPath += directries [j] + "/";
 }
 tmpPath = tmpPath.Remove (tmpPath.Length - 1, 1);
 if (!Directory.Exists (GetPath () + tmpPath)) {
 Directory.CreateDirectory (GetPath () + tmpPath);
 }
 }

 string existingString = OpenTextFile (GetPath () + dataPath);
 FileStream fs;
 if (existingString == "ERROR") {
 fs = new FileStream (GetPath () + dataPath, FileMode.CreateNew);
 } else {
 fs = new FileStream (GetPath () + dataPath, FileMode.Create);
 }
 StreamWriter sw = new StreamWriter (fs);
 sw.Write (stringData);
 sw.Flush ();
 sw.Close ();
 }

 public static void WriteData (string dataPath, List newData)
 {
 Debug.Log("[CsvManager.WriteData(string dataPath, List newData)] called");
 int maxLength = 1;
 for (int i = 0; i < newData.Count; i++) {
 if (maxLength < newData [i].Length) {
 maxLength = newData [i].Length;
 }
 }
 string stringData = "";
 for (int i = 0; i < newData.Count; i++) {
 for (int j = 0; j < maxLength; j++) {
 if (j < maxLength - 1) {
 stringData += newData [i] [j] + ",";
 } else if (j == maxLength - 1 && i < newData.Count - 1) {
 stringData += newData [i] [j] + "\ n";
 } else {
 stringData += newData [i] [j];
 }
 }
 }

 string[] directries = dataPath.Split ('/');
 for (int i = 0; i < directries.Length-1; i++) {
 string tmpPath = "";
 for (int j = 0; j < i + 1; j++) {
 tmpPath += directries [j] + "/";
 }
 tmpPath = tmpPath.Remove (tmpPath.Length - 1, 1);
 if (!Directory.Exists (GetPath () + tmpPath)) {
 Directory.CreateDirectory (GetPath () + tmpPath);
 }
 }
 string existingString = OpenTextFile (GetPath () + dataPath);
 FileStream fs;
 if (existingString == "ERROR") {
 fs = new FileStream (GetPath () + dataPath, FileMode.CreateNew);
 } else {
 fs = new FileStream (GetPath () + dataPath, FileMode.Create);
 }
 StreamWriter sw = new StreamWriter (fs);
 sw.Write (stringData);
 sw.Flush ();
 sw.Close ();
 }

 public static void WriteData (string dataPath, string data)
 {
 Debug.Log("[CsvManager.WriteData(string dataPath, string data)] called");
 string[] directries = dataPath.Split ('/');
 for (int i = 0; i < directries.Length-1; i++) {
 string tmpPath = "";
 for (int j = 0; j < i + 1; j++) {
 tmpPath += directries [j] + "/";
 }
 tmpPath = tmpPath.Remove (tmpPath.Length - 1, 1);
 if (!Directory.Exists (GetPath () + tmpPath)) {
 Directory.CreateDirectory (GetPath () + tmpPath);
 }
 }
 string existingString = OpenTextFile (GetPath () + dataPath);
 FileStream fs;
 if (existingString == "ERROR") {
 fs = new FileStream (GetPath () + dataPath, FileMode.CreateNew);
 } else {
 fs = new FileStream (GetPath () + dataPath, FileMode.Create);
 }
 StreamWriter sw = new StreamWriter (fs);
 sw.Write (data);
 sw.Flush ();
 sw.Close ();
 }


 public static string GetPath ()
 {
 #if UNITY_EDITOR
 return Application.dataPath + "/Resources/";
 #elif UNITY_ANDROID
 return Application.persistentDataPath + "/";
 #elif UNITY_IPHONE
 return Application.persistentDataPath + "/";
 #else
 return Application.dataPath + "";
 #endif
 }

 public static string OpenTextFile (string _filePath)
 {
 FileInfo fi = new FileInfo (_filePath);
 string returnSt = "";
 if (fi.Exists) {
 StreamReader sr = new StreamReader (fi.OpenRead (), Encoding.UTF8);
 returnSt = sr.ReadToEnd ();
 sr.Close ();
 } else {
 returnSt = "ERROR";
 }
 return returnSt;
 }
}



注2. AspectRatioManager

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

public class AspectRatioManager : MonoBehaviour {

 public float x_aspect = 1242f;
 public float y_aspect = 2208f;
 public CanvasScaler[] canvasScaler = new CanvasScaler[1];

 void Awake()
 {
 //Cameraのアスペクト比を設定する
 Camera camera = GetComponent();
 Rect rect = calcAspect(x_aspect, y_aspect);
 camera.rect = rect;

 //Canvasのアスペクト比を設定する
 for (int i = 0; i scale_height)
 {
 rect.x = 0;
 rect.y = (1.0f - scale_height) / 2.0f;
 rect.width = 1.0f;
 rect.height = scale_height;
 }
 else
 {
 float scale_width = 1.0f / scale_height;
 rect.x = (1.0f - scale_width) / 2.0f;
 rect.y = 0.0f;
 rect.width = scale_width;
 rect.height = 1.0f;
 }
 return rect;
 }


 ///


 /// Checks the screen ratio. Return 0 when width, 1 when height
 /// 
 private int CheckScreenRatio(int i){
 if (Screen.width * canvasScaler[i].referenceResolution.y / canvasScaler[i].referenceResolution.x < Screen.height) {
 return 0;
 } else {
 return 1;
 }
 }
}

注3. ListExtension

// ListExtension.cs
// http://kan-kikuchi.hatenablog.com/entry/ListExtension
//
// Created by kan.kikuchi on 2016.04.29.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

///


/// Listの拡張クラス
/// 
public static class ListExtension {

 //=================================================================================
 //重複
 //=================================================================================

 /// 
 /// 重複しないように追加
 /// 
 public static void AddToNotDuplicate(this List list, T t){
 if(list.Contains(t)){
 return;
 }
 list.Add (t);
 }
 
 /// 
 /// 重複を無くす
 /// 
 public static void RemoveDuplicate(this List list){
 List newList = new List();

 foreach (T item in list) {
 newList.AddToNotDuplicate(item);
 }

 list = newList;
 }

 //=================================================================================
 //並び変え
 //=================================================================================

 /// 
 /// ランダムに並び替え
 /// 
 public static List Shuffle(this List list){

 for (int i = 0; i < list.Count; i++) {
 T temp = list[i];
 int randomIndex = Random.Range(0, list.Count);
 list[i] = list[randomIndex];
 list[randomIndex] = temp;
 }

 return list;
 }

 //=================================================================================
 //取得
 //=================================================================================

 /// 
 /// 指定したNoのものを取得し、リストから消す
 /// 
 public static T GetAndRemove(this List list, int targetNo){
 if(list.Count <= targetNo || targetNo < 0){
 Debug.LogError ("リストの範囲を超えています!(ListCount : " + list.Count + ", No : " + targetNo + ")");
 }

 T target = list[targetNo];
 list.Remove (target);
 return target;
 }

 //=================================================================================
 //先入先出
 //=================================================================================

 /// 
 /// 先頭から取り出し、リストから消す
 /// 
 public static T Pop(this List list){
 return list.GetAndRemove(list.Count - 1);
 }

 //=================================================================================
 //後入先出
 //=================================================================================

 /// 
 /// 最後尾から取り出し、リストから消す
 /// 
 public static T Dequeue(this List list){
 return list.GetAndRemove(0);
 }

 //=================================================================================
 //ランダム取得
 //=================================================================================

 /// 
 /// ランダムに取得する
 /// 
 public static T GetAtRandom(this List list){
 if(list.Count == 0){
 Debug.LogError ("リストが空です!");
 }

 return list[Random.Range(0, list.Count)];
 }

 /// 
 /// ランダムに取得し、リストから消す
 /// 
 public static T GetAndRemoveAtRandom(this List list){
 T target = list.GetAtRandom ();
 list.Remove (target);
 return target;
 }

}