官网教程中script部分
没找到国内搬运带字幕视频和pdf(若有请务必通知我)
Quiz Game问答游戏
目录
Loading and Saving via Editor Script
Part1 Intro and Setup
游戏逻辑与游戏数据分离
基于简单UI
完成效果:
菜单场景(作为持久场景)
DataController持续存在于场景中
在游戏场景回答问题,计分、计时
游戏结束后可以回到menu
第一节课程的得分不存储,之后将会读取Json数据
Setup
分别创建预制件、场景、脚本文件夹
创建三个场景:
默认场景删除主相机,命名为持久场景(Persistent),
新建场景MenuScreen、Game,
保存在场景文件夹。
将分别完成Persistent、MenuScreen、Game设置
Persistent场景中:
创建DataController对象,附加DataController脚本,在三个场景之间保存数据
创建AnswerData脚本,作为纯数据类,只存储答案信息
Data Classes
编写AnswerData类
using UnityEngine;
using System.Collections;
[System.Serializable]//添加序列化则能编辑他们,在检查器inspector显示值
public class AnswerData //纯类不继承MonoBehaviour,类中没有方法
{
public string answerText;
public bool isCorrect;//该回答是否为正确的那个答案
}
创建QuestionData类并编写
using UnityEngine;
using System.Collections;
[System.Serializable]
public class QuestionData
{
public string questionText;
public AnswerData[] answers;//每个问题有一个答案数组
}
创建RoundData类并编写
using UnityEngine;
using System.Collections;
[System.Serializable]
public class RoundData
{
public string name;//玩到哪一关
public int timeLimitInSeconds;//每关限制用时
public int pointsAddedForCorrectAnswer;//回答正确得分分数
public QuestionData[] questions;//每关有一系列问题
}
编写DataController
提供显示关卡数据给GameController
using UnityEngine;
using System.Collections;
using UnityEngine.SceneManagement;//需要加载场景
public class DataController : MonoBehaviour //行为类,附加到对象
{
public RoundData[] allRoundData;//可以扩展为多轮游戏
// Use this for initialization
void Start ()
{
DontDestroyOnLoad (gameObject);//一般加载新场景时会销毁旧场景中所有对象。设置后加载时不再销毁该游戏对象,并被移动到单独的不破坏场景(DontDestroyOnLoad场景)
SceneManager.LoadScene ("MenuScreen");//然后加载初始场景
}
public RoundData GetCurrentRoundData()
{
return allRoundData [0];//只返回0的数据,稍后可传入索引值确定
}
// Update is called once per frame
void Update () {
}
}
Persistent添加DataController脚本
设置Menu相机背景为黑色
打开Build Setting,将所有场景放入(若不放入,调试运行时无法跳转)
运行看是否跳转到Menu,且保留对象
Menu Screen
在DataController脚本创建测试数据(改变数组长度后,可获得元素element并直接通过检测器更改)
打开Menu场景
添加UI->Button,重命名为StartButton
重置按钮位置Transform,设置按钮文本为Start Game,修改锚点为画布中心
UI->Text
设置文本Quiz Game 文本颜色白色 字号40 文字居中,拖动场景视图中的框可以直接修改文本框的位置与大小
创建空游戏对象,命名为MenuScreenController,在其上并添加新脚本组件,同名
MenuScreenController脚本:
using UnityEngine;
using System.Collections;
using UnityEngine.SceneManagement;//需要加载场景
public class MenuScreenController : MonoBehaviour {
public void StartGame()//public->该函数将被按钮调用
{
SceneManager.LoadScene("Game");
}
}
设置按钮的onClick事件:
+添加事件,拖动MenuScreenController对象到事件引用栏,下拉列表选择函数(如果只引用脚本,将无法找到函数)
运行测试是否跳转到Game场景
Game UI
打开Game场景
创建UI->Panel,重命名为QuestionPanel
在Panel的Image组件中颜色设置alpha to opaque(不透明-255),选择背景颜色
在QuestionPanel中添加UI文本,重命名为QuestionDisplay
字号32,颜色白色,锚点设置为左右拉伸
手动拖动锚点(更改高度),拖动边框矩形(左右留白,每侧32偏移),至效果如-
在QuestionPanel上添加另一个Panel,重命名为AnswerPanel
删除Image组件
下移锚点至Question文本下,拖动矩形框给相同留白,如下
在AnswerPanel上添加组件Vertical Layout Group(安排所有子UI元素)可以适当设置
如在AnswerPanel上添加按钮,按住Alt调整锚点,居中,则默认所有按钮均分Panel高度,占满宽度
当做为默认答案按钮,重命名为AnswerButton,按钮中字号20
在根画布(Canvas)下添加Panel,重命名为UI,
删除image组件,固定位置在问题文字上方
添加两个Text(TimeDisplay、ScoreDisplay),固定锚点在右上角,字色为白色
复制QuestionPanel,重命名为RoundOverPanel
将QuestionDisplay中的文字改为ROUND OVER,
删除其下的Panel和按钮
改变RoundOverPanel背景颜色(组件image)
添加MenuButton,文字为Menu
将UI面板拖到RoundOverPanel下方,保证它在顶部,在RoundOverPanel中也可以正常显示
默认情况下RoundOverPanel应该禁用,当结束游戏时激活
在最外层添加空游戏对象GameController、AnswerButtonObjectPool
Answer Button(设置对象组件文字,对象池)
使用和scroll list shop相同对象池with scroll list session
AnswerButtonObjectPool添加脚本SimpleObjectPool
ObjectPool是一种重用对象的方式,而不一直实例化+销毁
允许回收对象,避免垃圾回收机制、浪费
使用这个脚本需要引用Prefab(AnswerButton)
在将AnswerButton制作成预制件前需要先添加脚本:
在AnswerButton上添加脚本AnswerButton:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;//需要创建Text类型对象
public class AnswerButton : MonoBehaviour {
public Text answerText;//存储引用按钮文本,用它显示答案字符串
private AnswerData answerData;//私有变量存储答案数据
// Use this for initialization
void Start () {
}
public void Setup(AnswerData data)//设置按钮中答案数据
{
answerData = data;//接收data
answerText.text = answerData.answerText;//设置其中的answerText
}
// Update is called once per frame
void Update () {
}
}
为AnswerButton上的AnswerButton脚本的AnswerText栏添加引用:
AnswerButton的子对象Text
将AnswerButton拖入预制件文件夹,在层级文件夹中删除
将AnswerButton预制件拖入对象池AnswerButtonObjectPool上脚本simpleObjectPool的引用
SimpleObjectPool:
using UnityEngine;
using System.Collections.Generic;
// A very simple object pooling class
public class SimpleObjectPool : MonoBehaviour
{
// the prefab that this object pool returns instances of
public GameObject prefab;
// collection of currently inactive instances of the prefab
private Stack<GameObject> inactiveInstances = new Stack<GameObject>();
// Returns an instance of the prefab
public GameObject GetObject()
{
GameObject spawnedGameObject;
// if there is an inactive instance of the prefab ready to return, return that
if (inactiveInstances.Count > 0)
{
// remove the instance from teh collection of inactive instances
spawnedGameObject = inactiveInstances.Pop();
}
// otherwise, create a new instance
else
{
spawnedGameObject = (GameObject)GameObject.Instantiate(prefab);
// add the PooledObject component to the prefab so we know it came from this pool
PooledObject pooledObject = spawnedGameObject.AddComponent<PooledObject>();
pooledObject.pool = this;
}
// enable the instance
spawnedGameObject.SetActive(true);
// return a reference to the instance
return spawnedGameObject;
}
// Return an instance of the prefab to the pool
public void ReturnObject(GameObject toReturn)
{
PooledObject pooledObject = toReturn.GetComponent<PooledObject>();
// if the instance came from this pool, return it to the pool
if(pooledObject != null && pooledObject.pool == this)
{
// disable the instance
toReturn.SetActive(false);
// add the instance to the collection of inactive instances
inactiveInstances.Push(toReturn);
}
// otherwise, just destroy it
else
{
Debug.LogWarning(toReturn.name + " was returned to a pool it wasn't spawned from! Destroying.");
Destroy(toReturn);
}
}
}
// a component that simply identifies the pool that a GameObject came from
public class PooledObject : MonoBehaviour
{
public SimpleObjectPool pool;
}
Displaying Questions
在GameController对象上创建组件GameController脚本:
using System.Collections;
using System.Collections.Generic;//
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
public class GameController : MonoBehaviour {
public Text questionDisplayText;
public SimpleObjectPool answerButtonObjectPool;//按钮对象池
public Transform answerButtonParent;//获得父组件位置
private DataController dataController;//接收数据用
private RoundData currentRoundData;
private QuestionData[] questionPool;
private float timeRemaining;
private int playScore;
private int questionIndex;
private bool isRoundActive;//关卡正在进行还是结束(计时结束或问题完成)
private List<GameObject> answerButtonGameObjects=new List<GameObject>();//存储当前按钮列表,注意这里带复数s
// Use this for initialization
void Start () {
dataController = FindObjectOfType<DataController>();//得到并存储数据控制器对象的引用,不用担心找不到返回空,因为每次游戏从持久场景开始
currentRoundData = dataController.getCurrentRoundData ();//从数据控制器中获取当前关卡数据
questionPool = currentRoundData.questionData;
timeRemaining = currentRoundData.timeLimitInSecond;
playScore = 0;//初始化得分、问题下标、游戏状态
questionIndex = 0;
isRoundActive = true;
showQuestion ();
}
// Update is called once per frame
void Update () {
}
private void showQuestion (){//显示问题
RemoveAnswerButton ();//清空原有按钮,送回对象池
QuestionData questionData = questionPool [questionIndex];//当前索引问题数据(已从关卡数据中获得所有问题数据)
questionDisplayText.text = questionData.questionText;//接收要显示的问题文字,注意设置的是Text组件中的text属性
for (int i = 0; i < questionData.answerData.Length; i++) {//循环所有答案并根据需要添加按钮显示
GameObject answerButtonGameObject = answerButtonObjectPool.GetObject ();//从对象池拿到按钮对象,单个对象,无s
answerButtonGameObjects.Add (answerButtonGameObject);//将对象加入列表
answerButtonGameObject.transform.SetParent(answerButtonParent);//设置对象transform组件的属性(竖直布局)
AnswerButton answerButton = answerButtonGameObject.GetComponent<AnswerButton> ();// AnswerButton类得到对象中的脚本引用
answerButton.SetUp (questionData.answerData[i]);//并传入文本,调用设置
}
}
private void RemoveAnswerButton (){
while(answerButtonGameObjects.Count>0){
answerButtonObjectPool.ReturnObject(answerButtonGameObjects[0]);//按钮对象池收回对象,准备好重用
answerButtonGameObjects.RemoveAt (0);//从按钮对象列表中移除
}
}
}
Click To Answer
设置GameController中的变量:
QuestionText变量引用questionDisplay对象,
AnswerButtonObjectPool变量引用AnswerButtonObjectPool对象,
AnswerButtonParent变量引用AnswerPanel对象。
打开持久场景,测试游戏,可以看到第一个问题数据被提取,显示:
编辑GameController
脚本
,添加点击事件调用的函数AnswerButtonClicked(被按钮本身间接调用)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
public class GameController : MonoBehaviour {
public Text questionDisplayText;
public Text scoreDisplayText;
public GameObject questionDisplay;///引用两个Panel
public GameObject roundEndDisplay;
public SimpleObjectPool answerButtonObjectPool;
public Transform answerButtonParent;
private DataController dataController;
private RoundData currentRoundData;
private QuestionData[] questionPool;
private float timeRemaining;
private int playScore;
private int questionIndex;
private bool isRoundActive;
private List<GameObject> answerButtonGameObjects=new List<GameObject>();
// Use this for initialization
void Start () {
dataController = FindObjectOfType<DataController>();
currentRoundData = dataController.getCurrentRoundData ();
questionPool = currentRoundData.questionData;
timeRemaining = currentRoundData.timeLimitInSecond;
playScore = 0;
questionIndex = 0;
isRoundActive = true;
showQuestion ();
}
// Update is called once per frame
void Update () {
}
private void showQuestion (){
RemoveAnswerButton ();
QuestionData questionData = questionPool [questionIndex];
questionDisplayText.text = questionData.questionText;
for (int i = 0; i < questionData.answerData.Length; i++) {
GameObject answerButtonGameObject = answerButtonObjectPool.GetObject ();
answerButtonGameObjects.Add (answerButtonGameObject);
answerButtonGameObject.transform.SetParent(answerButtonParent);
AnswerButton answerButton = answerButtonGameObject.GetComponent<AnswerButton> ();
answerButton.SetUp (questionData.answerData[i]);
}
}
private void RemoveAnswerButton (){
while(answerButtonGameObjects.Count>0){
answerButtonObjectPool.ReturnObject(answerButtonGameObjects[0]);
answerButtonGameObjects.RemoveAt (0);
}
}
public void AnswerButtonClicked(bool isCorrect){/// 被按钮本身调用,必须为公共类,被调用时需要传入变量
if (isCorrect) {///回答正确则加分
playScore += currentRoundData.pointdAddedForCorrect;
scoreDisplayText.text = "Score:" + playScore.ToString();///更新分数文本对象的属性
}
if (questionPool.Length > questionIndex + 1) {///如果当前题后还有题(下标从0开始)
questionIndex++;
showQuestion ();///更新题目
} else {
EndRound ();///当前关卡结束
}
}
public void EndRound(){
isRoundActive = false;
questionDisplay.SetActive (false);///更换显示的Panel
roundEndDisplay.SetActive (true);
}
}
添加两个Panel、Score文本引用
编辑AnswerButton脚本:
添加GameController
引用变量
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class AnswerButton : MonoBehaviour {
public Text answerText;
private AnswerData answerData;
private GameController gameController;/// 添加GameController引用变量
// Use this for initialization
void Start () {
gameController = FindObjectOfType<GameController> ();///首先找到脚本类
}
public void SetUp(AnswerData data){
answerData = data;
answerText.text = answerData.answerText;
}
public void HandleClick(){///处理点击
gameController.AnswerButtonClicked (answerData.isCorrect);///传入当前答案是否正确
}
}
在预制件AnswerButton添加onClick事件,引用AnswerButton脚本,选择HandleClick事件
运行测试
如果在ShowQuestion函数中没有将answerButtonGameObject加入列表,则不能跟踪它,导致按钮不能被删除
//设置关卡结束时的回到菜单按钮
Ending The Game and Q&A倒计时
回到开始菜单,在MenuButton上添加事件,事件引用GameController对象
编写需要调用的方法public void ReturnToMenu(),并在按钮事件下拉列表选中
编写计时器
using UnityEngine;
using System.Collections;
using UnityEngine.UI;//
using UnityEngine.SceneManagement;//
using System.Collections.Generic;//泛型?
public class GameController : MonoBehaviour {
public Text questionDisplayText;//
public Text scoreDisplayText;///文本对象
public Text timeRemainingDisplayText;引用剩余时间对象
public SimpleObjectPool answerButtonObjectPool;//按钮对象池
public Transform answerButtonParent;//获得父组件位置
public GameObject questionDisplay;
public GameObject roundEndDisplay;///引用两个Panel
private DataController dataController;//接收数据用
private RoundData currentRoundData;//
private QuestionData[] questionPool;//
private bool isRoundActive;//关卡正在进行还是结束(计时结束或问题完成)
private float timeRemaining;//
private int questionIndex;//
private int playerScore;//
private List<GameObject> answerButtonGameObjects = new List<GameObject>();//存储当前按钮列表,注意这里带复数s
// Use this for initialization
void Start ()
{
dataController = FindObjectOfType<DataController> ();//得到并存储数据控制器对象的引用,不用担心找不到返回空,因为每次游戏从持久场景开始
currentRoundData = dataController.GetCurrentRoundData ();//从数据控制器中获取当前关卡数据
questionPool = currentRoundData.questions;//
timeRemaining = currentRoundData.timeLimitInSeconds;//
UpdateTimeRemainingDisplay();在开始时先显示max剩余时间
playerScore = 0;//初始化得分、问题下标、游戏状态
questionIndex = 0;//
ShowQuestion ();//
isRoundActive = true;//
}
private void ShowQuestion()//显示问题
{
RemoveAnswerButtons ();//清空原有按钮,送回对象池
QuestionData questionData = questionPool [questionIndex];//当前索引问题数据(已从关卡数据中获得所有问题数据)
questionDisplayText.text = questionData.questionText;//接收要显示的问题文字,注意设置的是Text组件中的text属性
for (int i = 0; i < questionData.answers.Length; i++)
{//循环所有答案并根据需要添加按钮显示
GameObject answerButtonGameObject = answerButtonObjectPool.GetObject();//从对象池拿到按钮对象,单个对象,无s
answerButtonGameObjects.Add(answerButtonGameObject);//将对象加入列表
answerButtonGameObject.transform.SetParent(answerButtonParent);//设置对象transform组件的属性(竖直布局)
AnswerButton answerButton = answerButtonGameObject.GetComponent<AnswerButton>();// AnswerButton类得到对象中的脚本引用
answerButton.Setup(questionData.answers[i]);//并传入文本,调用设置
}
}
private void RemoveAnswerButtons()//
{
while (answerButtonGameObjects.Count > 0)
{
answerButtonObjectPool.ReturnObject(answerButtonGameObjects[0]);//按钮对象池收回对象,准备好重用
answerButtonGameObjects.RemoveAt(0);//从按钮对象列表中移除
}
}
public void AnswerButtonClicked(bool isCorrect)
{/// 被按钮本身调用,必须为公共类,被调用时需要传入变量
if (isCorrect) ///回答正确则加分
{
playerScore += currentRoundData.pointsAddedForCorrectAnswer;
scoreDisplayText.text = "Score: " + playerScore.ToString();///更新分数文本对象的属性
}
if (questionPool.Length > questionIndex + 1) {///如果当前题后还有题(下标从0开始)
questionIndex++;
ShowQuestion ();///更新题目
} else
{
EndRound();///当前关卡结束
}
}
public void EndRound()///
{
isRoundActive = false;
questionDisplay.SetActive (false);///更换显示的Panel
roundEndDisplay.SetActive (true);
}
public void ReturnToMenu()注意返回菜单场景而不是持久场景,持久场景只在进入游戏的时候加载一次,确保仅有一个数据控制器实例
{
SceneManager.LoadScene ("MenuScreen");
}
private void UpdateTimeRemainingDisplay()
{
timeRemainingDisplayText.text = "Time: " + Mathf.Round (timeRemaining).ToString ();Mathf.Round转换为整数
}
// Update is called once per frame
void Update ()
{
if (isRoundActive) 确认游戏正在进行
{
timeRemaining -= Time.deltaTime; Time.deltaTime是最后一次渲染花费的时间
UpdateTimeRemainingDisplay();更新显示
if (timeRemaining <= 0f)同时通过时间控制游戏结束
{
EndRound();此处是结束游戏界面,不是菜单界面
}
}
}
}
设置GameController中变量引用(时间对象)
打开持久场景运行测试:游戏结束时停止计时
Intro To Part 2
在第一部分中分离了游戏逻辑与数据,一个所有数据独立的架构游戏逻辑
现在要从文本文件中加载数据,并使用JSON数据格式
JSON utility class、如何序列化、反序列化
制作最高分记录(保存简单信息)
编辑器脚本(Editor Script),在编辑器中加载游戏数据
前半部分完成版+所需界面优化下载链接:bit.ly/unityquizgame2
效果:
退出运行后重新运行,最高分可以保存
在Project视图StreamingAssets文件夹中存放data.json文件
直接修改文件后,运行游戏可以改变游戏中的数据
GameDataEditor:这个编辑器可以直接加载data文件中的数据、方便编辑
首先是存储、加载最高分
High Score with PlayerPrefs
创建脚本PlayerProgress(数据bean类,无方法):
public class PlayerProgress
{
public int highestScore = 0;
}
DataController脚本原本使用的是AllRoundData
编辑DataController脚本,加载PlayerProgress:
知识点:PlayerPrefs(Stores and accesses player preferences between game sessions.在会话之间存储和获得player preferences)
如果只有简单数据存储(如最高分)在进入、退出游戏时使用。适用于存储如音量等配置信息(它可读可编辑,如果担心作弊等问题,可将其二进制格式化-format)
工作方式:键值对。Key-highestScore,value保存每次的最高分
Static Methods:
DeleteAll DeleteKey
GetFloat GetInt(获取prefs中的值) GetString
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class DataController : MonoBehaviour {
public RoundData[] allRoundData;//在改成读文本形式后可改成private
private PlayerProgress playerProgress;//保存实例化对象
// Use this for initialization
void Start () {
DontDestroyOnLoad (gameObject);
LoadPlayerProgress ();//加载最高分数据
SceneManager.LoadScene ("MenuScreen");
}
public RoundData GetCurrentRoundData(){
return allRoundData[0];
}
// Update is called once per frame
void Update () {
}
//此函数可以很容易地扩展以处理我们想要存储在 PlayerProgress 对象中的任何其他数据
private void LoadPlayerProgress(){//加载PlayerProgress数据
// Create a new PlayerProgress object实例化
playerProgress = new PlayerProgress ();
//如果 playerprefs 包含 "最高分" 键, 则用与该键关联的值设置playerProgress.highestScore的值(加载最高分)
if (PlayerPrefs.HasKey ("highestScore")) {//确认是否有key
playerProgress.highestScore = PlayerPrefs.GetInt ("highestScore");//获取prefs中的值
}
}
public void SubmitNewPlayerScore(int newScore){//游戏结束时,游戏产生新的最高分,并存储//如果 newscore 大于原最高分, 使用新值更新playerProgress, 然后调用 SavePlayerProgress()
if (newScore > playerProgress.highestScore) {
playerProgress.highestScore = newScore;
SavePlayerProgress ();
}
}
private void SavePlayerProgress (){//存储最高分,需要key/value两个值
PlayerPrefs.SetInt ("highestScore",playerProgress.highestScore);
}
public int GetHeightestPlayerScore(){//在回合结束时需要获取、显示最高分
return playerProgress.highestScore;
}
}
将它与GameController挂钩,编辑GameController:
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
using System.Collections.Generic;
public class GameController : MonoBehaviour
{
public SimpleObjectPool answerButtonObjectPool;
public Text questionText;
public Text scoreDisplay;
public Text timeRemainingDisplay;
public Transform answerButtonParent;
public GameObject questionDisplay;
public GameObject roundEndDisplay;
public Text highScoreDisplay;//添加最高分显示文本对象
private DataController dataController;
private RoundData currentRoundData;
private QuestionData[] questionPool;
private bool isRoundActive = false;
private float timeRemaining;
private int playerScore;
private int questionIndex;
private List<GameObject> answerButtonGameObjects = new List<GameObject>();
void Start()
{
dataController = FindObjectOfType<DataController>(); // Store a reference to the DataController so we can request the data we need for this round
currentRoundData = dataController.GetCurrentRoundData(); // Ask the DataController for the data for the current round. At the moment, we only have one round - but we could extend this
questionPool = currentRoundData.questions; // Take a copy of the questions so we could shuffle the pool or drop questions from it without affecting the original RoundData object
timeRemaining = currentRoundData.timeLimitInSeconds; // Set the time limit for this round based on the RoundData object
UpdateTimeRemainingDisplay();
playerScore = 0;
questionIndex = 0;
ShowQuestion();
isRoundActive = true;
}
void Update()
{
if (isRoundActive)
{
timeRemaining -= Time.deltaTime; // If the round is active, subtract the time since Update() was last called from timeRemaining
UpdateTimeRemainingDisplay();
if (timeRemaining <= 0f) // If timeRemaining is 0 or less, the round ends
{
EndRound();
}
}
}
void ShowQuestion()
{
RemoveAnswerButtons();
QuestionData questionData = questionPool[questionIndex]; // Get the QuestionData for the current question
questionText.text = questionData.questionText; // Update questionText with the correct text
for (int i = 0; i < questionData.answers.Length; i ++) // For every AnswerData in the current QuestionData...
{
GameObject answerButtonGameObject = answerButtonObjectPool.GetObject(); // Spawn an AnswerButton from the object pool
answerButtonGameObjects.Add(answerButtonGameObject);
answerButtonGameObject.transform.SetParent(answerButtonParent);
answerButtonGameObject.transform.localScale = Vector3.one;
AnswerButton answerButton = answerButtonGameObject.GetComponent<AnswerButton>();
answerButton.SetUp(questionData.answers[i]); // Pass the AnswerData to the AnswerButton so the AnswerButton knows what text to display and whether it is the correct answer
}
}
void RemoveAnswerButtons()
{
while (answerButtonGameObjects.Count > 0) // Return all spawned AnswerButtons to the object pool
{
answerButtonObjectPool.ReturnObject(answerButtonGameObjects[0]);
answerButtonGameObjects.RemoveAt(0);
}
}
public void AnswerButtonClicked(bool isCorrect)
{
if (isCorrect)
{
playerScore += currentRoundData.pointsAddedForCorrectAnswer; // If the AnswerButton that was clicked was the correct answer, add points
scoreDisplay.text = playerScore.ToString();
}
if(questionPool.Length > questionIndex + 1) // If there are more questions, show the next question
{
questionIndex++;
ShowQuestion();
}
else // If there are no more questions, the round ends
{
EndRound();
}
}
private void UpdateTimeRemainingDisplay()
{
timeRemainingDisplay.text = Mathf.Round(timeRemaining).ToString();
}
public void EndRound()
{
isRoundActive = false;
dataController.SubmitNewPlayerScore (playerScore);//在关卡结束时提交分数
highScoreDisplay.text = dataController.GetHeightestPlayerScore ().ToString ();//更新文本显示
questionDisplay.SetActive(false);
roundEndDisplay.SetActive(true);
}
public void ReturnToMenu()
{
SceneManager.LoadScene("MenuScreen");
}
}
添加最高分text引用
运行游戏测试,分数能够更新:
Serialization and Game Data
需要编辑DataController,从JSON加载所有游戏数据
目前是在编辑器的数据控制器设置。
序列化过程:对象转换成存储字节流,为了存储该字节流对象,传送到数据库或文件。
主要目的是,保存对象的状态,在需要时重新创造(逆过程称为逆序列化)MSDN
将一系列数据作为一个对象(GameData类型的对象)->将它转化为JSON文本称作序列化
在需要时反序列化把数据加载回来,填充GameData的字段
Json是JS对象表示法,一种数据交互格式,可以在不同的语言中交互数据
创建新C#脚本:GameData
(将要序列化和反序列化的类,没有变量)
也可以将类本身序列化。放在单独的Data中可以随时添加别的数据
可以序列化的简单字段类型:
具有该Serializable属性的自定义非抽象非泛型类
具有该Serializable属性的自定义结构
对从UnityEngine.Object派生的对象的引用
原始数据类型(int,float,double,bool,string,等等)
枚举类型
某些Unity内置类型:Vector2,Vector3,Vector4,Rect,Quaternion,Matrix4x4,Color,Color32,LayerMask,AnimationCurve,Gradient,RectOffset,GUIStyle
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[System.Serializable] //可序列化
public class GameData{
public RoundData[] allRoundData; //在DataController中也定义了相同的变量
}
编写JSON, DataController加载
Json例:
{"allRoundData":[{"name":"Animals","timeLimitInSeconds":20,"pointsAddedForCorrectAnswer":10,"questions":[{"questionText":"Lions are carnivores: true or false?","answers":[{"answerText":"True","isCorrect":true},{"answerText":"False","isCorrect":false}]},{"questionText":"What do frogs eat?","answers":[{"answerText":"Pizza","isCorrect":false},{"answerText":"Flies","isCorrect":true}]},{"questionText":"Where do mice live?","answers":[{"answerText":"In the sea","isCorrect":false},{"answerText":"On the moon","isCorrect":false},{"answerText":"On land","isCorrect":true},{"answerText":"In a tree","isCorrect":false}]}]}]} |
Loading Game Data via JSON
有了GameData对象,从JSON
在DataController脚本-中添加函数加载游戏数据:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using System.IO;///加载时需要用到,否则不能用Path类
public class DataController : MonoBehaviour {
public RoundData[] allRoundData;
private PlayerProgress playerProgress;
private string gameDataFileName = "data.json";///数据文件
// Use this for initialization
void Start () {
DontDestroyOnLoad (gameObject);
LoadGameData ();///在开始时调用加载数据
LoadPlayerProgress ();
SceneManager.LoadScene ("MenuScreen");
}
public RoundData GetCurrentRoundData(){
return allRoundData[0];
}
// Update is called once per frame
void Update () {
}
private void LoadPlayerProgress(){
playerProgress = new PlayerProgress ();
if (PlayerPrefs.HasKey ("highestScore")) {
playerProgress.highestScore = PlayerPrefs.GetInt ("highestScore");
}
}
public void SubmitNewPlayerScore(int newScore){
if (newScore > playerProgress.highestScore) {
playerProgress.highestScore = newScore;
SavePlayerProgress ();
}
}
private void LoadGameData (){///加载GameData数据
string filePath = Path.Combine (Application.streamingAssetsPath,gameDataFileName);// Path.Combine将字符串合并为一个文件Application.StreamingAssets指向Assets/StreamingAssets在编辑器中, 以及 "StreamingAssets" 文件夹in a build
// streamingAssetsPath是StreamingAssets文件夹,使用这个路径,因为不同平台的路径可能会不同,将它与文件名结合
if (File.Exists (filePath)) {// Read the json from the file into a string获得文件中的JSON字符串
string dataAsJson = File.ReadAllText (filePath);// Pass the json to JsonUtility, and tell it to create a GameData object from it格式化字符串为原对象
GameData loadedData = JsonUtility.FromJson<GameData> (dataAsJson);//读取数据转换类型
// Retrieve the allRoundData property of loadedData检索其中的allRoundData属性
allRoundData = loadedData.allRoundData;
} else {
Debug.LogError ("can not load game data");
}
}
private void SavePlayerProgress (){
PlayerPrefs.SetInt ("highestScore",playerProgress.highestScore);
}
public int GetHeightestPlayerScore(){
return playerProgress.highestScore;
}
}
运行游戏,测试可以加载数据
Loading and Saving via Editor Script
在编辑窗口编辑数据,通过编辑器脚本-Editor Script
在Script文件夹创建Editor文件夹(放在这个文件夹中才有效),新建GameDataEditor脚本:
编写两个Editor窗口实际执行的函数-LoadGameData从JSON文件加载数据到编辑器、SaveGameData反之
下一节中用UI代码编写按钮等
Game Data Editor GUI
显示编辑器窗口,从菜单中打开窗口
using UnityEngine;
using UnityEditor;//编写Editor需要,可获得EditorWindow
using System.Collections;
using System.IO;//需要使用文件
public class GameDataEditor : EditorWindow//继承编辑窗口
{
public GameData gameData;//public变量,将被序列化,并在编辑器编辑
private string gameDataProjectFilePath = "/StreamingAssets/data.json";//
[MenuItem ("Window/Game Data Editor")]//在用户在菜单中点击该项时调用这个函数
static void Init()//
{
EditorWindow.GetWindow (typeof(GameDataEditor)).Show ();//打开窗口
}
void OnGUI()//绘制按钮,inspector
{
if (gameData != null) // gameData对象存在,显示保存按钮,否则可能保存空数据
{
SerializedObject serializedObject = new SerializedObject (this);//创建编辑器的序列化对象,显示游戏数据对象
SerializedProperty serializedProperty = serializedObject.FindProperty ("gameData");//序列化属性
EditorGUILayout.PropertyField (serializedProperty, true);//设置显示而非折叠其子字段
serializedObject.ApplyModifiedProperties ();提交用户改变的属性,更新序列化对象
if (GUILayout.Button ("Save data"))//两个按钮分别调用函数
{
SaveGameData();
}
}
if (GUILayout.Button ("Load data"))
{
LoadGameData();
}
}
private void LoadGameData()//文件->编辑器 反序列化,与DataController相似
{
string filePath = Application.dataPath + gameDataProjectFilePath;//生成文件路径,需要文件名变量,放在方法外会找不到
if (File.Exists (filePath)) {
string dataAsJson = File.ReadAllText (filePath);
gameData = JsonUtility.FromJson<GameData> (dataAsJson);//反序列化,接收转换的game Data,需要在外声明
} else
{
gameData = new GameData();//没有数据则新建空对象
}
}
private void SaveGameData()//序列化
{
string dataAsJson = JsonUtility.ToJson (gameData);//对象转换为JSON
string filePath = Application.dataPath + gameDataProjectFilePath;
File.WriteAllText (filePath, dataAsJson); //存储JSON字符串,如果文件已存在,将覆盖它
}
}
写完初始化方法后测试菜单有显示GameDataEditor项,打开后显示空窗口(2:10)
全部写完后可以打开窗口,Load data按钮加载数据,更改后save data,
编译为PC执行文件,
在项目文件夹新建Build文件夹放置编译文件,输入文件名,编译完成后:
在data文件夹可以看到StreamAssets文件夹(其中有data.json, EXE执行时读取)
QA
可以在文档查看JsonUtility类,可以转换json与对象转换
从json转换为对象时,非序列化属性将被忽略,如果json缺少值,则赋默认值
必须从主线程调用文本资产(TextAssets)
生成JSON文件可以通过以上编写的序列化窗口填写属性,不需要手动写json
也可以使用可以web应用程序,放入数据库