一、原版修改
1、导入资源
Unity Learn | 3D Beginner: Complete Project | URP
2、设置Scene
删除SampleScene,打开UnityTechnologies-3DBeginnerComplete下的MainScene
3、降低音量
(1) 打开Hierarchy面板上的Audio降低音量
(2) 打开Prefabs文件夹,找到Ghost,降低音量
4、给FaderCanvas添加组件
(1) Canvas Scaler
(2) Graphic Raycaster
二、新增收集事件
1、新增物体
(1) 新增物体:UI-Image。Questions。设置它的Rect Transform。
(2) 新增文本:ItemsText, TipText(可设置有透明度的黑色背景)
2、新建CollectedItems.cs
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
public class CollectedItems : MonoBehaviour
{
public QuestionCollision QuestionCollision;
//public GameObject Questions;
public TextMeshProUGUI ItemsText, TipText;
static int nucleus=0, mitochondria = 0, GolgiApparatus = 0, endoplasmicReticulum = 0, ribosome = 0;
void Start()
{
if (ItemsText == null || TipText == null)
{
Debug.LogError("Questions或ItemsText 或 TipText 未被正确分配!");
}
}
private void OnTriggerEnter(Collider other)
{
if (other.CompareTag("Player"))
{
RandomEvent();
}
}
public void RandomEvent()
{
int numericValue = Random.Range(0, 100);
if (numericValue < 5 && nucleus==0)
{
nucleus = 1;
TipText.text = "你发现了细胞核!";
}
else if(numericValue < 26)
{
mitochondria++;
TipText.text = "你得到了线粒体!";
}
else if (numericValue < 41)
{
endoplasmicReticulum++;
TipText.text = "你得到了内质网*1!";
}
else if (numericValue < 56)
{
GolgiApparatus++;
TipText.text = "你得到了高尔基体*1!";
}
else if (numericValue < 76)
{
ribosome++;
TipText.text = "你得到了核糖体*1!";
}
else if (numericValue < 86)
{
QuestionCollision.ShowQuestions(true);
//Questions.SetActive(true);
}
else
{
TipText.text = "你什么都没有发现……";
}
ItemsUpdate();
}
void ItemsUpdate()
{
ItemsText.text = "当前持有:细胞核" + nucleus + ";线粒体" + mitochondria + ";高尔基体" + GolgiApparatus + ";内质网" + endoplasmicReticulum + ";核糖体" + ribosome;
}
}
using System;
using System.Collections;
using System.Collections.Generic;
using TMPro;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.UI;
using static UnityEngine.Rendering.DebugUI;
public class Observer : MonoBehaviour
{
public Exercises exercises;
public Transform player;
public GameObject Questions;
public TextMeshProUGUI ItemsText, TipText;
static int nucleus=0, mitochondria = 0, GolgiApparatus = 0, endoplasmicReticulum = 0, ribosome = 0;
bool canTriggerEffect = true;
void OnTriggerEnter(Collider other)
{
if (other.CompareTag("Player")) // 玩家的标签为 "Player"
{
exercises.ShowQuestion(0);
RandomEvent();
}
}
public void QuitBtn()
{
GetComponent<UnityEngine.UI.Button>().gameObject.SetActive(false);
Questions.SetActive(false);
}
void ItemsUpdate()
{
ItemsText.text =
"当前持有:细胞核" + nucleus + ";线粒体" + mitochondria + ";高尔基体" + GolgiApparatus + ";内质网" + endoplasmicReticulum + ";核糖体" + ribosome;
}
void Start()
{
if (ItemsText == null || TipText == null)
{
Debug.LogError("ItemsText 或 TipText 未被正确分配!");
}
}
void RandomEvent()
{
if (canTriggerEffect)
{
// 触发效果代码
int numericValue = UnityEngine.Random.Range(0, 100);
if (numericValue < 5 && nucleus == 0)//获取的随机数范围在0~4
{
nucleus = 1;
TipText.text = "你发现了细胞核!";
StartCoroutine(ClearTextRoutine(2f));
print("随机数" + numericValue);
}
else if (numericValue < 21)
{
mitochondria++;
TipText.text = "你得到了线粒体!";
StartCoroutine(ClearTextRoutine(2f));
print("随机数" + numericValue);
}
else if (numericValue < 41)
{
endoplasmicReticulum++;
TipText.text = "你得到了内质网*1!";
StartCoroutine(ClearTextRoutine(2f));
print("随机数" + numericValue);
}
else if (numericValue < 61)
{
GolgiApparatus++;
TipText.text = "你得到了高尔基体*1!";
StartCoroutine(ClearTextRoutine(2f));
print("随机数" + numericValue);
}
else if (numericValue < 81)
{
ribosome++;
TipText.text = "你得到了核糖体*1!";
StartCoroutine(ClearTextRoutine(2f));
print("随机数" + numericValue);
}
else if (numericValue < 96)
{
Questions.SetActive(true);
}
else
{
TipText.text = "你什么都没有发现……";
StartCoroutine(ClearTextRoutine(2f));
print("随机数" + numericValue);
}
//gameEnding.CaughtPlayer ();
ItemsUpdate();
canTriggerEffect = false; // 触发效果,将canTriggerEffect设置为false
StartCoroutine(CooldownRoutine()); // 启动冷却时间协程
}
}
IEnumerator CooldownRoutine()
{
yield return new WaitForSeconds(60f); // 等待1分钟
canTriggerEffect = true; // 冷却结束,可以再次触发效果
}
IEnumerator ClearTextRoutine(float delay)
{
yield return new WaitForSeconds(delay);
TipText.text = "";
}
}
3、更改enemy设置
(1) 复制 PointOfView预制体,得到PointOfView(1),更名为CollectedItems
(2) 移除CollectedItems上的Observer.cs,添加CollectedItems.cs
(3) 删除Hierarchy面板上的除Ghost (3)以外的敌人的PointOfView。添加CollectedItems预制体并赋值
三、编辑答题面板预制体
1、设置面板
(1) UI-Image,命名为Questions。
(2) 选中导入的图片,然后在Inspector面板中将Texture Type设置为Sprite (2D and UI)
(3) 更改Questions的Image,Transform
(4) 添加按钮,QuitBtn(以隐藏答题面板)
2、添加test
(1) 添加文本。TitleText
(2) 添加按钮。OptionBtnA,按钮下文本的名字OptionTextA,更改文字字体,复制这个按钮得到四个选项按钮
(3) 将Questions制成预制体
(4) 制作题目
3、建立题库——简单题库
A. 创建ChoiceQuestion.cs(不用挂载的脚本)可以与其他脚本放在一起
定义问题类(包含一个问题(question
字段)、四个选项(options
数组)和正确答案的索引(correctOptionIndex
字段))
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[System.Serializable]
public class Question
{
public string question; // 问题文本
public string[] options = new string[4]; // 选项数组,假设每个问题都有4个选项
public int correctOptionIndex; // 正确答案的索引(0表示A,1表示B,依此类推)
// 构造函数(可选),用于初始化问题
public Question(string q, string[] opts, int correctIndex)
{
question = q;
options = opts;
correctOptionIndex = correctIndex;
}
// 检查答案是否正确
public bool CheckAnswer(int playerChoice)
{
return playerChoice == correctOptionIndex;
}
}
B. 在一个空物体上添加Exercises.cs,控制显示题目和选项、正误判断、答题后禁用按钮
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public class Exercises : MonoBehaviour
{
public Database_01 Database_01;//题库脚本
public TextMeshProUGUI questionText;
public Button[] optionButtons;
private int currentQuestionIndex = 0; // 初始化当前问题索引为0
public void ShowQuestion(int index)
{
if (index >= 0 && index < Database_01.questionsDatabase.Count)//如果index在questionsDatabase列表的有效范围内
{
// 显示题目(从列表中取出索引为index的问题,并将其文本内容显示在questionText的文本组件上)
questionText.text = Database_01.questionsDatabase[index].question;
// 显示选项(将Database_01中当前问题的选项文本显示在optionButtons数组或列表中的相应UI元素上)
for (int i = 0; i < Database_01.questionsDatabase[index].options.Length; i++)
{
optionButtons[i].GetComponentInChildren<TextMeshProUGUI>().text = Database_01.questionsDatabase[index].options[i];
}
}
}
// 选项按钮的点击事件
public void OnOptionButtonClicked(int buttonIndex)
{
foreach (var button in optionButtons)
{
button.interactable = false; // 设置为false以禁用按钮
}
// currentQuestionIndex:当前显示的问题的索引
// 检查用户选择的答案是否正确
bool isCorrect = Database_01.questionsDatabase[currentQuestionIndex].CheckAnswer(buttonIndex);
// 根据检查结果执行相应的操作,比如更新UI、显示消息等
if (isCorrect)
{
Debug.Log("回答正确!");
currentQuestionIndex++; // 更新当前问题索引
}
else
{
Debug.Log("回答错误!");
}
}
}
C.给空物体添加Database_01.cs,以建立题库
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Database_01 : MonoBehaviour
{
public List<Question> questionsDatabase = new List<Question>(); // 选择题数据库
void Start()
{
// 初始化选择题数据库
InitializeQuestions();
}
void InitializeQuestions()
{
//添加一个问题到数据库
questionsDatabase.Add(new Question(
"…………是?",
new string[] { "A. ……", "B. ……", "C. ……", "D. ……" },
0 // 假设"选项A"是正确答案,所以索引是0
));
//添加一个问题到数据库
questionsDatabase.Add(new Question(
"…………",
new string[] { "A. 复杂性", "B. 多样性", "C. 统一性", "D. 稳定性" },
2 // 假设"选项C"是正确答案,所以索引是2
));
}
}
4、建立题库——导入txt文本的方式
(1) 加载文本
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using TMPro;
using UnityEngine;
public class QuizManager : MonoBehaviour
{
private Question currentQuestion;//存储当前展示的题目
private List<Question> questionList;
public TextMeshProUGUI questionTextComponent; // 用于显示题干的Text组件
public TextMeshProUGUI optionATextComponent; // 用于显示选项A的Text组件
public TextMeshProUGUI optionBTextComponent; // 用于显示选项B的Text组件
public TextMeshProUGUI optionCTextComponent; // 用于显示选项C的Text组件
public TextMeshProUGUI optionDTextComponent; // 用于显示选项D的Text组件
void Start()
{
questionList = new List<Question>();
LoadQuestionsFromTxt("QuestionBank.txt");
GetRandomQuestion();
}
//加载文本文件
void LoadQuestionsFromTxt(string filename)
{
// 组合文件的完整路径。Application.streamingAssetsPath是Unity的一个属性,
// 指向StreamingAssets文件夹的路径
string filePath = Path.Combine(Application.streamingAssetsPath, filename);
// 检查文件是否存在
if (File.Exists(filePath))
{
// 使用File.OpenText来打开文件,并创建一个StreamReader来读取内容
using (StreamReader reader = File.OpenText(filePath))
{
// 声明一个字符串变量line,用于存储从文件中读取的每一行内容
string line;
// 逐行读取文件内容,直到读取到文件末尾
while ((line = reader.ReadLine()) != null)
{
// 使用 | 分隔每一行的内容
string[] data = line.Split('|');
// 检查是否至少有6个由竖线分隔的值(问题、四个选项和答案)
if (data.Length < 6)
{
Debug.LogError("Invalid line format in question file: " + line);
continue; // 跳过这一行,继续读取下一行
}
// 从分割后的数组中提取问题、选项和答案
string question = data[0];
string optionA = "A. " + data[1];
string optionB = "B. " + data[2];
string optionC = "C. " + data[3];
string optionD = "D. " + data[4];
string answer = data[5].ToUpper();
// 检查答案是否在给定的选项中
if (!IsValidAnswer(answer, optionA, optionB, optionC, optionD))
{
Debug.LogError("Invalid answer in question file: " + line);
continue; // 跳过这一行,继续读取下一行
}
// 创建一个新的Question对象,并将其添加到questionList列表中
Question newQuestion = new Question(question, optionA, optionB, optionC, optionD, answer.ToUpper());
questionList.Add(newQuestion);
}
}
}
else
{
Debug.LogError("Cannot find question file: " + filename);
}
}
// 检查答案是否在给定的选项列表中
bool IsValidAnswer(string answer, string optionA, string optionB, string optionC, string optionD)
{
//StringComparison.OrdinalIgnoreCase:确保比较时不区分大小写
return answer.Equals("A", StringComparison.OrdinalIgnoreCase) ||
answer.Equals("B", StringComparison.OrdinalIgnoreCase) ||
answer.Equals("C", StringComparison.OrdinalIgnoreCase) ||
answer.Equals("D", StringComparison.OrdinalIgnoreCase);
}
//从题目列表中随机选择一个题目,并将其赋值给currentQuestion变量
public void GetRandomQuestion()
{
if (questionList.Count == 0)
{
Debug.LogError("No questions to display.");
return;
}
int randomIndex = UnityEngine.Random.Range(0, questionList.Count);
currentQuestion = questionList[randomIndex];
questionList.RemoveAt(randomIndex);// 从列表中移除问题,防止重复
}
void UpdateQuestionDisplay()
{
// 分别设置各个Text组件的text属性
questionTextComponent.text = currentQuestion.QuestionText;
optionATextComponent.text = currentQuestion.OptionAText;
optionBTextComponent.text = currentQuestion.OptionBText;
optionCTextComponent.text = currentQuestion.OptionCText;
optionDTextComponent.text = currentQuestion.OptionDText;
}
void OnUserAnswer()
{
// 处理用户答题后的操作
// 获取下一个随机题目
GetRandomQuestion();
// 更新问题展示
UpdateQuestionDisplay();
}
}
// 定义一个名为Question的类,用于存储问题及其相关信息
[Serializable]
public class Question
{
public string QuestionText;
public string OptionAText;
public string OptionBText;
public string OptionCText;
public string OptionDText;
public string Answer; // 注意这里不需要ToUpper(),除非您有特定的需求
public Question(string question, string optionA, string optionB, string optionC, string optionD, string answer)
{
QuestionText = question;
OptionAText = optionA;
OptionBText = optionB;
OptionCText = optionC;
OptionDText = optionD;
Answer = answer;
}
}
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using TMPro;
using UnityEngine;
public class QuizManager : MonoBehaviour
{
private Question currentQuestion;//存储当前展示的题目
private List<Question> questionList;
public TextMeshProUGUI questionText;
void Start()
{
questionList = new List<Question>();
LoadQuestionsFromTxt("QuestionBank.txt");
GetRandomQuestion();
}
//加载文本文件
void LoadQuestionsFromTxt(string filename)
{
// 组合文件的完整路径。Application.streamingAssetsPath是Unity的一个属性,
// 指向StreamingAssets文件夹的路径
string filePath = Path.Combine(Application.streamingAssetsPath, filename);
// 检查文件是否存在
if (File.Exists(filePath))
{
// 使用File.OpenText来打开文件,并创建一个StreamReader来读取内容
using (StreamReader reader = File.OpenText(filePath))
{
// 声明一个字符串变量line,用于存储从文件中读取的每一行内容
string line;
// 逐行读取文件内容,直到读取到文件末尾
while ((line = reader.ReadLine()) != null)
{
// 使用 | 分隔每一行的内容
string[] data = line.Split('|');
// 检查是否至少有6个由竖线分隔的值(问题、四个选项和答案)
if (data.Length < 6)
{
Debug.LogError("Invalid line format in question file: " + line);
continue; // 跳过这一行,继续读取下一行
}
// 从分割后的数组中提取问题、选项和答案
string question = data[0];
string optionA = "A. " + data[1];
string optionB = "B. " + data[2];
string optionC = "C. " + data[3];
string optionD = "D. " + data[4];
string answer = data[5].ToUpper();
// 检查答案是否在给定的选项中
if (!IsValidAnswer(answer, optionA, optionB, optionC, optionD))
{
Debug.LogError("Invalid answer in question file: " + line);
continue; // 跳过这一行,继续读取下一行
}
// 创建一个新的Question对象,并将其添加到questionList列表中
Question newQuestion = new Question(question, optionA, optionB, optionC, optionD, answer.ToUpper());
questionList.Add(newQuestion);
}
}
}
else
{
Debug.LogError("Cannot find question file: " + filename);
}
}
// 检查答案是否在给定的选项列表中
bool IsValidAnswer(string answer, string optionA, string optionB, string optionC, string optionD)
{
//StringComparison.OrdinalIgnoreCase:确保比较时不区分大小写
return answer.Equals("A", StringComparison.OrdinalIgnoreCase) ||
answer.Equals("B", StringComparison.OrdinalIgnoreCase) ||
answer.Equals("C", StringComparison.OrdinalIgnoreCase) ||
answer.Equals("D", StringComparison.OrdinalIgnoreCase);
}
//从题目列表中随机选择一个题目,并将其赋值给currentQuestion变量
public void GetRandomQuestion()
{
if (questionList.Count == 0)
{
Debug.LogError("No questions to display.");
return;
}
int randomIndex = UnityEngine.Random.Range(0, questionList.Count);
currentQuestion = questionList[randomIndex];
questionList.RemoveAt(randomIndex);// 从列表中移除问题,防止重复
string displayText = currentQuestion.QuestionText + "\n" + // 假设Question类有一个QuestionText属性
currentQuestion.OptionAText + "\n" + // 类似地,其他选项和答案也需要有相应的属性
currentQuestion.OptionBText + "\n" +
currentQuestion.OptionCText + "\n" +
currentQuestion.OptionDText;
questionText.text = displayText;
}
void UpdateQuestionDisplay()
{
// 将currentQuestion的问题文本赋值
// 将currentQuestion的问题文本赋值给questionText的text属性
questionText.text = currentQuestion.QuestionText;
}
void OnUserAnswer()
{
// 处理用户答题后的操作
// 获取下一个随机题目
GetRandomQuestion();
// 更新问题展示
UpdateQuestionDisplay();
}
}
// 定义一个名为Question的类,用于存储问题及其相关信息
[Serializable]
public class Question
{
public string QuestionText;
public string OptionAText;
public string OptionBText;
public string OptionCText;
public string OptionDText;
public string Answer; // 注意这里不需要ToUpper(),除非您有特定的需求
public Question(string question, string optionA, string optionB, string optionC, string optionD, string answer)
{
QuestionText = question;
OptionAText = optionA;
OptionBText = optionB;
OptionCText = optionC;
OptionDText = optionD;
Answer = answer;
}
}
(1)
(2) QuestionBank.txt
文件位于Unity项目的StreamingAssets
文件夹中,并且格式正确(即每行都有六个由 | 分隔的值)答案用ABCD表示
(3)
(4)
4、设置调用test
(1) 给JohnLemon添加PlayerQuest.cs
5、判断正误
6、生命值脚本
using UnityEngine;
public class PlayerHealth : MonoBehaviour
{
public int maxLives = 3;
private int currentLives;
private void Start()
{
currentLives = maxLives;
}
public void AddLife()
{
if (currentLives < maxLives)
{
currentLives++;
}
}
public void LoseLife()
{
if (currentLives > 0)
{
currentLives--;
}
else
{
Debug.Log("角色死亡!退出游戏");
}
}
}
7、问题面板显示和隐藏脚本
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public class QuestionCollision : MonoBehaviour
{
public GameObject questionPanel;//问题面板
public Button QuitBtn;
public TextMeshProUGUI questionText;//问题文本
public Button[] optionBtns;
public PlayerHealth playerHealth;
public void ShowQuestions(bool isQuestionActivated)
{
if (isQuestionActivated)
{
questionPanel.SetActive(true);
}
else
{
questionPanel.SetActive(false);
}
foreach (Button button in optionBtns)
{
button.onClick.AddListener(()=> CheckAnswer(button));
}
}
public void OnClickQuitBtn()
{
questionPanel.SetActive(false);
}
void Start()
{
questionPanel.SetActive(false);
}
private void CheckAnswer(Button button)
{
if (button.CompareTag("CorrectAnswer"))
{
playerHealth.AddLife();
}
questionPanel.SetActive(false);
}
}
public List<Transform> bodyList = new List<Transform>();
public GameObject bodyPrefab;
public Sprite[] bodySprites = new Sprite[2];//数组中只含有2张图片
private Transform canvas;
private void Awake()
{
canvas = GameObject.Find("Canvas").transform;//找到Canvas的Transform
}
void BodyCreate()
{
//如果bodyList的元素数量是偶数,Index将被赋值为0。如果bodyList的元素数量是奇数,Index将被赋值为1
int Index = (bodyList.Count % 2 == 0) ? 0 : 1;//bodyList.Count:列表中的元素数量(生成几种物体)
//bodyList.Count % 2:元素数量除以2,的余数
//bodyList.Count % 2 == 0:检查余数是否是0(是0,说明元素数量是偶数)
//? 0 : 1:如果元素数量是偶数(余数是0)则整个表达式的值是0。否则是1
GameObject body = Instantiate(bodyPrefab,new Vector3(2000,2000,0),Quaternion.identity);//生成预制体在看不到的位置
body.GetComponent<UnityImage>().sprite = bodySprites[Index];//从bodySprites数组中获取位于Index位置的图片
body.transform.SetParent(canvas, false);//使Canvas成为body的父物体,而不使用它的世界坐标
bodyList.Add(body.transform);
}
ItemsBackground,ItemsText,TipText