Unity 4.Quiz Game(持久场景、数据类、按钮事件、UI布局、对象池、倒计时、PlayerPrefs、序列化、加载JSON数据)

官网教程中script部分

没找到国内搬运带字幕视频和pdf(若有请务必通知我)

Quiz Game问答游戏

目录

Part1 Intro and Setup

Setup

Data Classes

Game UI

Answer Button(设置对象组件文字,对象池)

Displaying Questions

Click To Answer

Ending The Game and Q&A倒计时

Intro To Part 2

High Score with PlayerPrefs

Serialization and Game Data

Loading Game Data via JSON

Loading and Saving via Editor Script

Game Data Editor GUI

QA

至此进度项目下载


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,且保留对象

在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应用程序,放入数据库

至此进度项目下载

https://download.csdn.net/download/lagoon_lala/10939856

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值