突然想用unity制作一个答题系统,于是就去做了。
一、首先介绍答题系统的功能:
1、本地csv题库文件读取
2、上、下题目切换
3、单选、多选题目的判定读取
4、答案提示
5、提交
6、考试倒计时
7、准确率计分
二、项目构建过程
1、构建一个canvas和canvas的子物体panel(大背景面板)、RawImage、一个
用来介绍本系统的textmeshpro组件,text里写“答题系统”。
顺便放3个按钮,分别为上一题(P-Button)、下一题(N-Button)、交卷(T-Button)、提示按钮(Tip-Button)、答题进度textmeshpro组件(progressText)、分数textmeshpro组件(resultText)、答案提示文本组件(tip-Text (TMP))、3D空物体(QuizManager)用来挂载我们的脚本。
2、再在canvas里面创建一个panel来放我们项目里的各种UI
包括:放置题目的Text (TMP)组件、空物体(命名为:ToggleGroup)并加上ToggleGroup组件,用来放我们的选项组,在这个空物体里面创建多个toggle组件。再在这个canvas里面再创建一个Slider组件用来显示倒计时,顺便再放置一个文本(textmeshpro)组件,文本显示倒计时。
页面搭建完,UI位置和大概情况是这样的:
在这里我解释一下我为什么文本组件不喜欢用unity本身默认的text组件的原因:因为unity本身的text组件会有文本放大失真的体验,但是在某种程度上它可以有效减少GPU和CPU的消耗,因为功能较少。
而使用textmeshpro组件的话,首先要构建好一个textmeshpro字体文件,具体的构建字体资源文件操作大家可以去看看别的博主教程,比较简单。这里就不做过多介绍。然后将字体资源放到组件中:
提示一下:如果不构建textmeshpro组件的中文字体资源,将会造成中文字符显示异常,显示为框框的现象。
再一次的提醒大家对于项目中的架构层级,请严格检查几次,防止出现因为其它UI遮挡而造成按钮点击没反应的情况。
2、在项目文件夹下新建创建一个Questions文件夹并在里面创建一个csv文件,命名为:Excel-Questions.csv其文件内容及格式是这样的(大家可以按文件内容格式填写相关数据):
注意:咱们的csv文件和unity自动生成的C#脚本默认都是GB2312格式的。
其实建议大家去使用UTF-8格式文件用以读取和解析,因为通用,如果使用UTF-8格式那么请对于文件保存格式请再三确定为CSV的UTF-8格式如下图:
3、将按钮的点击事件给匹配上
比如:将下一题的按钮点击事件点击加号,加上一个监听事件,然后将挂载脚本的QuizManager空物体放到里面,再在右侧的监听函数里面选择我们对应按钮的OnNextButtonClicked()函数即可。
其它几个按钮也是一样的操作,不过监听函数不同罢了,代码里都有说明。
三、脚本逻辑实现
GB2312读取脚本如下:
using UnityEngine;
using UnityEngine.UI;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using TMPro;
public class QuizSystem : MonoBehaviour
{
// UI元素
public TextMeshProUGUI questionText; // 显示问题文本
public ToggleGroup optionsGroup; // 选项组,用于单选题
public List<Toggle> optionToggles; // 选项切换按钮列表
public TextMeshProUGUI progressText; // 显示答题进度
public TextMeshProUGUI resultText; // 显示结果文本
public TextMeshProUGUI hintText; // 显示提示文本
public Button nextButton; // 下一题按钮
public Button previousButton; // 上一题按钮
public Button submitButton; // 提交按钮
public Button hintButton; // 提示按钮
public Slider timeSlider; // 时间滑动条
public TextMeshProUGUI timerText; // 倒计时文本
// 问题列表和当前状态
private List<Question> questions = new List<Question>(); // 问题列表
private int currentQuestionIndex = 0; // 当前问题索引
private List<HashSet<int>> userAnswers = new List<HashSet<int>>(); // 用户答案列表
private bool isSubmitted = false; // 是否已提交
// 总体倒计时
private float totalTime = 1800f; // 总时间(秒)
private float remainingTime; // 剩余时间
void Start()
{
// 加载问题
LoadQuestions(Application.dataPath + "/Questions/Excel-Questions.csv");
// 初始化用户答案列表
for (int i = 0; i < questions.Count; i++)
{
userAnswers.Add(new HashSet<int>());
}
// 显示第一个问题
DisplayQuestion();
// 初始化倒计时
remainingTime = totalTime;
UpdateTimerUI();
}
void Update()
{
// 更新倒计时
if (remainingTime > 0)
{
remainingTime -= Time.deltaTime;
UpdateTimerUI();
}
else if (!isSubmitted)
{
// 时间到,自动提交
OnSubmitButtonClicked();
}
}
void LoadQuestions(string filePath)
{
// 检查文件是否存在
if (!File.Exists(filePath))
{
Debug.LogError("没有在 " + filePath+ "下找到题库文件!");
return;
}
// 读取文件内容
Encoding encoding = Encoding.GetEncoding("GB2312");
string[] lines = File.ReadAllLines(filePath, encoding);
// 解析每一行
for (int i = 1; i < lines.Length; i++)
{
string[] parts = lines[i].Split(',');
if (parts.Length < 7) continue;
// 创建问题对象
Question question = new Question
{
Text = parts[0],
Options = new string[] { parts[1], parts[2], parts[3], parts[4] },
IsSingleChoice = parts[5] == "1",
CorrectAnswers = new HashSet<int>(Array.ConvertAll(parts[6].ToCharArray(), c => int.Parse(c.ToString())))
};
questions.Add(question);
}
}
void DisplayQuestion()
{
// 检查是否超出问题列表
if (currentQuestionIndex >= questions.Count)
{
return;
}
// 获取当前问题
Question question = questions[currentQuestionIndex];
// 添加题目类型到问题文本
string questionType = question.IsSingleChoice ? "(单选题)" : "(多选题)";
questionText.text = $"{currentQuestionIndex + 1}. {question.Text} {questionType}";
progressText.text = $"答题进度: {GetAnsweredCount()} / {questions.Count}";
// 显示选项
for (int i = 0; i < optionToggles.Count; i++)
{
if (i < question.Options.Length)
{
optionToggles[i].gameObject.SetActive(true);
var label = optionToggles[i].GetComponentInChildren<Text>();
if (label != null)
{
label.text = question.Options[i];
}
optionToggles[i].isOn = userAnswers[currentQuestionIndex].Contains(i + 1);
optionToggles[i].group = question.IsSingleChoice ? optionsGroup : null;
}
else
{
optionToggles[i].gameObject.SetActive(false);
}
}
// 隐藏提示文本
hintText.gameObject.SetActive(false);
}
void UpdateTimerUI()
{
// 更新滑动条和倒计时文本
timeSlider.value = remainingTime / totalTime;
timerText.text = $"剩余时间: {Mathf.Ceil(remainingTime)}秒";
}
public void OnNextButtonClicked()
{
// 切换到下一题
if (currentQuestionIndex < questions.Count - 1)
{
SaveUserAnswers();
currentQuestionIndex++;
DisplayQuestion();
isSubmitted = false; // 重置提交状态
hintText.gameObject.SetActive(false);
}
}
public void OnPreviousButtonClicked()
{
// 切换到上一题
if (currentQuestionIndex > 0)
{
SaveUserAnswers();
currentQuestionIndex--;
DisplayQuestion();
isSubmitted = false; // 重置提交状态
hintText.gameObject.SetActive(false);
}
}
public void OnSubmitButtonClicked()
{
// 提交答案
if (!isSubmitted)
{
SaveUserAnswers();
CheckAllAnswers();
foreach (Toggle toggle in optionToggles)
{
toggle.interactable = false;
}
isSubmitted = true;
// 隐藏提示文本
hintText.gameObject.SetActive(false);
}
}
public void OnHintButtonClicked()
{
// 显示正确答案提示
hintText.gameObject.SetActive(true);
Question question = questions[currentQuestionIndex];
string correctAnswerLetters = ConvertNumbersToLetters(question.CorrectAnswers);
hintText.text = $"答案为: {correctAnswerLetters}";
}
void SaveUserAnswers()
{
// 保存用户选择的答案
userAnswers[currentQuestionIndex].Clear();
int index = 1;
foreach (Toggle toggle in optionToggles)
{
if (toggle.isOn)
{
userAnswers[currentQuestionIndex].Add(index);
}
index++;
}
}
void CheckAllAnswers()
{
// 检查所有答案并计算正确率
int correctCount = 0;
for (int i = 0; i < questions.Count; i++)
{
if (userAnswers[i].SetEquals(questions[i].CorrectAnswers))
{
correctCount++;
}
}
resultText.text = $"正确率: {(float)correctCount / questions.Count * 100:F2}%";
}
int GetAnsweredCount()
{
// 获取已回答的问题数量
int count = 0;
foreach (var answer in userAnswers)
{
if (answer.Count > 0)
{
count++;
}
}
return count;
}
string ConvertNumbersToLetters(HashSet<int> numbers)
{
// 将数字转换为字母
StringBuilder letters = new StringBuilder();
foreach (int number in numbers)
{
if (number >= 1 && number <= 4)
{
char letter = (char)('A' + number - 1);
letters.Append(letter);
}
}
return letters.ToString();
}
}
public class Question
{
public string Text; // 问题文本
public string[] Options; // 选项数组
public bool IsSingleChoice; // 是否为单选题
public HashSet<int> CorrectAnswers; // 正确答案集合
}
utf-8格式读取csv数据,并且如果你需要打包成exe文件并在外部运行打包生成的exe文件时成功读取到csv数据那么请你也使用这一版代码(当然如果你只是引擎内部运行,那么请随意):
首先需要做一个小小的改动:
1、在Unity项目的Assets目录下创建一个名为StreamingAssets的文件夹,并将UTF-8格式的CSV文件(例如UTF-8-Excel-Questions)放入其中。
接下来就是代码更改了如下:
using UnityEngine;
using UnityEngine.UI;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using TMPro;
public class QuizSystem : MonoBehaviour
{
// UI元素
public TextMeshProUGUI questionText; // 显示问题文本
public ToggleGroup optionsGroup; // 选项组,用于单选题
public List<Toggle> optionToggles; // 选项切换按钮列表
public TextMeshProUGUI progressText; // 显示答题进度
public TextMeshProUGUI resultText; // 显示结果文本
public TextMeshProUGUI hintText; // 显示提示文本
public Button nextButton; // 下一题按钮
public Button previousButton; // 上一题按钮
public Button submitButton; // 提交按钮
public Button hintButton; // 提示按钮
public Slider timeSlider; // 时间滑动条
public TextMeshProUGUI timerText; // 倒计时文本
// 问题列表和当前状态
private List<Question> questions = new List<Question>(); // 问题列表
private int currentQuestionIndex = 0; // 当前问题索引
private List<HashSet<int>> userAnswers = new List<HashSet<int>>(); // 用户答案列表
private bool isSubmitted = false; // 是否已提交
// 总体倒计时
private float totalTime = 1800f; // 总时间(秒)
private float remainingTime; // 剩余时间
void Start()
{
// 加载问题
LoadQuestions();
// 初始化用户答案列表
for (int i = 0; i < questions.Count; i++)
{
userAnswers.Add(new HashSet<int>());
}
// 显示第一个问题
DisplayQuestion();
// 初始化倒计时
remainingTime = totalTime;
UpdateTimerUI();
}
void Update()
{
// 更新倒计时
if (remainingTime > 0)
{
remainingTime -= Time.deltaTime;
UpdateTimerUI();
}
else if (!isSubmitted)
{
// 时间到,自动提交
OnSubmitButtonClicked();
}
}
void LoadQuestions()
{
// 构建文件路径
string filePath = Path.Combine(Application.streamingAssetsPath, "Excel-Questions.csv");
// 检查文件是否存在
if (!File.Exists(filePath))
{
Debug.LogError("Questions file not found at: " + filePath);
hintText.text = "无法找到问题文件,请检查文件路径是否正确。";
return;
}
// 使用UTF-8编码读取文件内容
Encoding encoding = Encoding.UTF8;
string[] lines = File.ReadAllLines(filePath, encoding);
// 解析每一行
for (int i = 1; i < lines.Length; i++)
{
string[] parts = lines[i].Split(',');
if (parts.Length < 7) continue;
// 创建问题对象
Question question = new Question
{
Text = parts[0],
Options = new string[] { parts[1], parts[2], parts[3], parts[4] },
IsSingleChoice = parts[5] == "1",
IsJudgment = parts[5] == "2", // 判断题标识
CorrectAnswers = new HashSet<int>(Array.ConvertAll(parts[6].ToCharArray(), c => int.Parse(c.ToString())))
};
questions.Add(question);
}
}
void DisplayQuestion()
{
// 检查是否超出问题列表
if (currentQuestionIndex >= questions.Count)
{
return;
}
// 获取当前问题
Question question = questions[currentQuestionIndex];
questionText.text = question.Text;
progressText.text = $"答题进度: {GetAnsweredCount()} / {questions.Count}";
// 显示选项
for (int i = 0; i < optionToggles.Count; i++)
{
if (i < question.Options.Length && (!question.IsJudgment || i < 2))
{
optionToggles[i].gameObject.SetActive(true);
var label = optionToggles[i].GetComponentInChildren<Text>();
if (label != null)
{
label.text = question.Options[i];
}
optionToggles[i].isOn = userAnswers[currentQuestionIndex].Contains(i + 1);
// 判断题或单选题使用 ToggleGroup
optionToggles[i].group = (question.IsSingleChoice || question.IsJudgment) ? optionsGroup : null;
}
else
{
optionToggles[i].gameObject.SetActive(false);
}
}
// 隐藏提示文本
hintText.gameObject.SetActive(false);
}
void UpdateTimerUI()
{
// 更新滑动条和倒计时文本
if (!isSubmitted) // 仅在未提交时更新
{
timeSlider.value = remainingTime / totalTime;
timerText.text = $"剩余时间: {Mathf.Ceil(remainingTime)}秒";
}
}
public void OnNextButtonClicked()
{
// 切换到下一题
if (currentQuestionIndex < questions.Count - 1)
{
SaveUserAnswers();
currentQuestionIndex++;
DisplayQuestion();
isSubmitted = false; // 重置提交状态
hintText.gameObject.SetActive(false);
}
}
public void OnPreviousButtonClicked()
{
// 切换到上一题
if (currentQuestionIndex > 0)
{
SaveUserAnswers();
currentQuestionIndex--;
DisplayQuestion();
isSubmitted = false; // 重置提交状态
hintText.gameObject.SetActive(false);
}
}
public void OnSubmitButtonClicked()
{
// 提交答案
if (!isSubmitted)
{
SaveUserAnswers();
CheckAllAnswers();
foreach (Toggle toggle in optionToggles)
{
toggle.interactable = false;
}
isSubmitted = true;
// 隐藏提示文本
hintText.gameObject.SetActive(false);
// 锁定滑块位置
LockSliderPosition();
}
}
void LockSliderPosition()
{
// 禁用滑块交互
timeSlider.interactable = false;
}
void StopTimer()
{
// 停止倒计时并更新文本
timerText.text = $"本次用时:{totalTime - remainingTime:F0}s";
timeSlider.value = 0; // 停止滑动条
}
public void OnHintButtonClicked()
{
// 显示正确答案提示
hintText.gameObject.SetActive(true);
Question question = questions[currentQuestionIndex];
string correctAnswerLetters = ConvertNumbersToLetters(question.CorrectAnswers);
hintText.text = $"答案为: {correctAnswerLetters}";
}
void SaveUserAnswers()
{
// 保存用户选择的答案
userAnswers[currentQuestionIndex].Clear();
int index = 1;
foreach (Toggle toggle in optionToggles)
{
if (toggle.isOn)
{
userAnswers[currentQuestionIndex].Add(index);
}
index++;
}
}
void CheckAllAnswers()
{
// 检查所有答案并计算正确率
int correctCount = 0;
for (int i = 0; i < questions.Count; i++)
{
// 检查用户答案是否为正确答案的子集
if (questions[i].CorrectAnswers.IsSubsetOf(userAnswers[i]))
{
correctCount++;
}
}
resultText.text = $"正确率: {(float)correctCount / questions.Count * 100:F2}%";
}
int GetAnsweredCount()
{
// 获取已回答的问题数量
int count = 0;
foreach (var answer in userAnswers)
{
if (answer.Count > 0)
{
count++;
}
}
return count;
}
string ConvertNumbersToLetters(HashSet<int> numbers)
{
// 将数字转换为字母
StringBuilder letters = new StringBuilder();
foreach (int number in numbers)
{
if (number >= 1 && number <= 4)
{
char letter = (char)('A' + number - 1);
letters.Append(letter);
}
}
return letters.ToString();
}
}
public class Question
{
public string Text; // 问题文本
public string[] Options; // 选项数组
public bool IsSingleChoice; // 是否为单选题
public HashSet<int> CorrectAnswers; // 正确答案集合
public bool IsJudgment; // 是否为判断题
}
打包出来运行就OK了!和下面的运行结果一样。
四、视频效果展示
20241021_163344