用Unity开发一个答题系统

突然想用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

### 创建或完善基于Unity答题系统 #### 功能概述 为了构建一个高效且灵活的答题系统,在Unity环境中,开发者可以获得一套完整的资源文件来直接使用。这套系统不仅易于集成至现有项目中,还支持多种题型的选择,如选择题、填空题以及判断题等,极大地满足了不同场景下的需求[^1]。 #### 技术实现要点 ##### 集成与初始化 当准备将此答题模块加入到自己的游戏中时,需先从指定位置下载对应的资源包,并将其顺利导入Unity工程里。完成上述操作之后,按照具体的要求调整好各项设置参数,便能迅速启动并运行这一组件。 ##### 用户交互逻辑处理 对于按钮点击事件响应机制而言,`ButtonItem`类展示了基本的设计模式。通过挂接在各个选项上的脚本实例化对象监听用户的触碰动作,进而触发相应的回调函数执行特定的任务流程。例如: ```csharp using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class ButtonItem : MonoBehaviour { private Button btn; void Start() { btn = GetComponent<Button>(); btn.onClick.AddListener(OnButtonClick); } public void OnButtonClick() { Panel_Question.GetInstance().HandleClick(this); } } ``` 这里对原始代码进行了优化命名以便更好地理解其意图所在[^2]。 ##### 数据存储与管理 考虑到实际应用中的复杂性和多样性,内部集成了简易的数据管理体系允许创作者便捷地维护试题库的内容——无论是新增还是修改都变得轻而易举。此外,未来还可以考虑引入外部数据源(像Excel文档),进一步增强系统的可拓展性[^4]。 #### 自定义与扩展能力 除了提供默认风格外,也鼓励使用者依据自身的创意去改造原有的图形用户界面(UI),使之更加贴合目标受众群体喜好;同时开放API接口供第三方插件接入,共同打造丰富多彩的学习娱乐体验环境。
评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值