クイズ

クイズゲームを作ろう！

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


目次

ステップ① スプレッドシート（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でクイズを１問だけ表示させてみましょう。

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

















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

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





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


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


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

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


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

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


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





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

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

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

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

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




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

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

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

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




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


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









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

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


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






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

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

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

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
























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


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




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






















コピペファイル一覧

注1. CsvManager

using UnityEngine; using System.Collections; using System.IO; using System.Collections.Generic; using System.Text;   public class CsvManager { 	/// <summary> 	/// Resourcesフォルダ内のcsvファイルを読み込み、string型2次元配列で返す。 	/// example) CsvManager.ReadTextFile("Commands/BasicCommands.csv") 	/// </summary> 	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; 	}   	/// <summary> 	/// Resourcesフォルダ内に、ファイルを書き込む。 	/// 例) csvManager.WriteData("data.csv", dataArray); 	/// </summary> 	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<string[]> newData) 	{ 		Debug.Log("[CsvManager.WriteData(string dataPath, List<string[]> 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<Camera>(); 		Rect rect = calcAspect(x_aspect, y_aspect); 		camera.rect = rect;  		//Canvasのアスペクト比を設定する 		for (int i = 0; i<canvasScaler.Length; i++) { 			canvasScaler [i].matchWidthOrHeight = CheckScreenRatio (i); 		} 	}  	private Rect calcAspect(float width, float height) 	{ 		float target_aspect = width / height; 		float window_aspect = (float)Screen.width / (float)Screen.height; 		float scale_height = window_aspect / target_aspect; 		Rect rect = new Rect(0.0f, 0.0f, 1.0f, 1.0f);  		if(1.0f > 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; 	}   	/// <summary> 	/// Checks the screen ratio. Return 0 when width, 1 when height 	/// </summary> 	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;  /// <summary> /// Listの拡張クラス /// </summary> public static class ListExtension {    //=================================================================================   //重複   //=================================================================================    /// <summary>   /// 重複しないように追加   /// </summary>   public static void AddToNotDuplicate<T>(this List<T> list, T t){     if(list.Contains(t)){       return;     }     list.Add (t);   }      /// <summary>   /// 重複を無くす   /// </summary>   public static void RemoveDuplicate<T>(this List<T> list){     List<T> newList = new List<T>();      foreach (T item in list) {       newList.AddToNotDuplicate(item);     }      list = newList;   }    //=================================================================================   //並び変え   //=================================================================================    /// <summary>   /// ランダムに並び替え   /// </summary>   public static List<T> Shuffle<T>(this List<T> 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;   }    //=================================================================================   //取得   //=================================================================================    /// <summary>   /// 指定したNoのものを取得し、リストから消す   /// </summary>   public static T GetAndRemove<T>(this List<T> 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;   }    //=================================================================================   //先入先出   //=================================================================================    /// <summary>   /// 先頭から取り出し、リストから消す   /// </summary>   public static T Pop<T>(this List<T> list){     return list.GetAndRemove(list.Count - 1);   }    //=================================================================================   //後入先出   //=================================================================================    /// <summary>   /// 最後尾から取り出し、リストから消す   /// </summary>   public static T Dequeue<T>(this List<T> list){     return list.GetAndRemove(0);   }    //=================================================================================   //ランダム取得   //=================================================================================    /// <summary>   /// ランダムに取得する   /// </summary>   public static T GetAtRandom<T>(this List<T> list){     if(list.Count == 0){       Debug.LogError ("リストが空です！");     }      return list[Random.Range(0, list.Count)];   }    /// <summary>   /// ランダムに取得し、リストから消す   /// </summary>   public static T GetAndRemoveAtRandom<T>(this List<T> list){     T target = list.GetAtRandom ();     list.Remove (target);     return target;   }  }