一、原版修改
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) UI-Image,命名为Questions。
(2) 选中导入的图片,然后在Inspector面板中将Texture Type设置为Sprite (2D and UI)
(3) 更改Questions的Image,Transform
(4) 添加按钮,QuitBtn(以隐藏答题面板)
(5) 添OptionBtnA(ABCD)
(6) 新增文本:TipAnswerText、TitleText
三、建立和调用题库
1、创建Question.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Question
{
public string titleIndex, questiont, correctOptionIndex; // 题号,问题文本, 正确答案的索引(1表示A,2表示B,依此类推)
public string optionA, optionB, optionC, optionD;// 选项
// 构造函数(可选),用于初始化问题
public Questions(string titleInx, string q, string optsA, string optsB, string optsC, string optsD, string correctIndex)
{
titleIndex = titleInx;
questiont = q;
optionA = optsA;
optionB = optsB;
optionC = optsC;
optionD = optsD;
correctOptionIndex = correctIndex;
}
// 检查答案是否正确
public bool CheckAnswer(string playerChoice)
{
return playerChoice == correctOptionIndex;
}
}
或——建议这个
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[System.Serializable]
public class Question
{
public string questionTitle, questionTitleIndex; // 问题文本、题号
public string[] options = new string[4]; // 选项数组,假设每个问题都有4个选项
public int correctOptionIndex; // 正确答案的索引(1表示A,2表示B,依此类推)
// 构造函数(可选),用于初始化问题
public Question(string questionID, string questionText, string[] options, int correctOptionIndex)
{
this.questionTitleIndex = questionID;
this.questionTitle = questionText;
this.options = options;
this.correctOptionIndex = correctOptionIndex;
}
}
2、加载题库并随机显示
using System;
using System.Collections;
using System.Collections.Generic;
using System.Xml;
using TMPro;
using UnityEngine;
public class LoadQuestions : MonoBehaviour
{
//存放问题的容器
public TextMeshProUGUI titleText, optionTextA, optionTextB, optionTextC, optionTextD;
private void Start()
{
LoadQuestionsFromFile("YW");
}
public void LoadQuestionsFromFile(string filePath)
{
//加载位于Resources文件夹中的YW文件
TextAsset textFile = Resources.Load<TextAsset>("YW");
//若文件加载成功
if (textFile != null)
{
Debug.Log("YW文件加载成功");
//将textFile中储存的文本按行拆分成一个字符串数组,每个数组元素都表示一行文本("\n":换行符)
//string[] lines = textFile.text.Split("new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None");
//不同系统或编辑器可能使用的不同换行符格式
string[] lines = textFile.text.Split("\n");
// 获取一个随机的索引
int randomIndex = UnityEngine.Random.Range(0, lines.Length);
//从lines中获取一个随机元素,将之赋值给line
string line = lines[randomIndex];
//解析line中的字段
string[] fields = line.Split(":");
// 创建问题对象
Questions question = new(fields[0], fields[1], fields[2], fields[3], fields[4], fields[5], fields[6]);
titleText.text = question.questiont;
optionTextA.text = question.optionA;
optionTextB.text = question.optionB;
optionTextC.text = question.optionC;
optionTextD.text = question.optionD;
}
else { Debug.Log("YW文件加载失败"); }
}
}
或 ——建议下面这个(需要在Unity编辑器中设置选项按钮的On Click
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class LoadQuestions : MonoBehaviour
{
public Text titleText;
public Button[] optionButtons;
private string[] lines; // 保存所有问题的数组
private List<Question> questions = new List<Question>(); // 问题列表
private Question currentQuestion;
private void Start()
{
LoadQuestionsFromFile();
UpdateQuestionUI();
}
//加载题库
public void LoadQuestionsFromFile()
{
TextAsset textFile = Resources.Load<TextAsset>("test_01");
if (textFile != null)
{
lines = textFile.text.Split("\n");
for (int i = 0; i < lines.Length; i++)
{
string[] fields = lines[i].Split(":");
if (fields.Length >= 7)
{
string questionID = fields[0];
string questionTitle = fields[1];
string[] options = new string[] { fields[2], fields[3], fields[4], fields[5] };
//int correctOptionIndex = int.Parse(fields[6]); // 将正确答案的索引作为整数类型存储
int correctOptionIndex;
if (int.TryParse(fields[6], out correctOptionIndex)) // 使用 TryParse 避免异常
{
questions.Add(new Question(questionID, questionTitle, options, correctOptionIndex));
}
else
{
Debug.LogError("文件格式错误:第 " + (i + 1) + " 行中的正确答案索引不是有效的整数");
}
}
else
{
Debug.LogError("文件格式错误:第 " + (Array.IndexOf(lines, lines) + 1) + " 行没有足够的字段");
}
}
}
}
//随机显示
public void UpdateQuestionUI()
{
int randomIndex = UnityEngine.Random.Range(0, questions.Count - 1);
currentQuestion = questions[randomIndex];
string question = currentQuestion.questionTitle;
string[] options = currentQuestion.options; // 获取选项数组
int correctIndex = currentQuestion.correctOptionIndex; // 获取正确选项的索引
titleText.text = question;
for (int i = 0; i < optionButtons.Length; i++)
{
optionButtons[i].onClick.RemoveAllListeners();
int index = i; // 创建局部变量保存索引值
optionButtons[i].GetComponentInChildren<Text>().text = options[i];
}
}
}
3、判断正误
public void CheckAnswer(int optionIndex)
{
if (optionIndex == currentQuestion.correctOptionIndex)
{
Debug.Log("回答正确!" + optionIndex + currentQuestion.correctOptionIndex);
}
else
{
Debug.Log("回答错误!" + optionIndex + currentQuestion.correctOptionIndex);
}
}
4、禁用按钮
public void UpdateQuestionUI()
{
int randomIndex = UnityEngine.Random.Range(0, questions.Count - 1);
currentQuestion = questions[randomIndex];
string question = currentQuestion.questionTitle;
string[] options = currentQuestion.options; // 获取选项数组
int correctIndex = currentQuestion.correctOptionIndex; // 获取正确选项的索引
titleText.text = question;
for (int i = 0; i < optionButtons.Length; i++)
{
optionButtons[i].onClick.RemoveAllListeners();
int index = i; // 创建局部变量保存索引值
optionButtons[i].GetComponentInChildren<Text>().text = options[i];
optionButtons[i].interactable = true; // 启用按钮
}
}
public void CheckAnswer(int optionIndex)
{
for (int i = 0; i < optionButtons.Length; i++)
{
optionButtons[i].interactable = false; // 禁用按钮
}
if (optionIndex == currentQuestion.correctOptionIndex)
{
Debug.Log("回答正确!" + optionIndex + currentQuestion.correctOptionIndex);
}
else
{
Debug.Log("回答错误!" + optionIndex + currentQuestion.correctOptionIndex);
}
}
5、记录答过的题目,并从题库中清除
public void LoadQuestionsFromFile()
{
TextAsset textFile = Resources.Load<TextAsset>("test_01");
if (textFile != null)
{
//确保跨平台兼容性拆分文本
string[] lines = textFile.text.Split(new string[] { System.Environment.NewLine }, System.StringSplitOptions.RemoveEmptyEntries);
for (int i = 0; i < lines.Length; i++)
{
string[] fields = lines[i].Split(':');
if (questions.Count > 0)
{
usedQuestions.Clear();
}
if (fields.Length >= 7)
{
string questionID = fields[0];
string questionTitle = fields[1];
string[] options = new string[] { fields[2], fields[3], fields[4], fields[5] };
int correctOptionIndex;
if (int.TryParse(fields[6], out correctOptionIndex))
{
questions.Add(new Question(questionID, questionTitle, options, correctOptionIndex));
}
else
{
Debug.LogErrorFormat("文件格式错误:第 {0} 行中的正确答案索引不是有效的整数", i + 1);
}
}
else
{
Debug.LogErrorFormat("文件格式错误:第 {0} 行没有足够的字段", i + 1);
}
}
}
else
{
Debug.LogError("找不到文本文件 test_01");
}
}
完整脚本
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class LoadQuestions : MonoBehaviour
{
public Text titleText;
public Button[] optionButtons;
private string[] lines; // 保存所有问题的数组
private List<Question> questions = new List<Question>(); // 问题列表
private Question currentQuestion;
private void Start()
{
LoadQuestionsFromFile();
UpdateQuestionUI();
}
public void LoadQuestionsFromFile()
{
TextAsset textFile = Resources.Load<TextAsset>("test_01");
if (textFile != null)
{
lines = textFile.text.Split("\n");
for (int i = 0; i < lines.Length; i++)
{
string[] fields = lines[i].Split(":");
if (fields.Length >= 7)
{
string questionID = fields[0];
string questionTitle = fields[1];
string[] options = new string[] { fields[2], fields[3], fields[4], fields[5] };
//int correctOptionIndex = int.Parse(fields[6]); // 将正确答案的索引作为整数类型存储
int correctOptionIndex;
if (int.TryParse(fields[6], out correctOptionIndex)) // 使用 TryParse 避免异常
{
questions.Add(new Question(questionID, questionTitle, options, correctOptionIndex));
}
else
{
Debug.LogError("文件格式错误:第 " + (i + 1) + " 行中的正确答案索引不是有效的整数");
}
}
else
{
Debug.LogError("文件格式错误:第 " + (Array.IndexOf(lines, lines) + 1) + " 行没有足够的字段");
}
}
}
}
public void UpdateQuestionUI()
{
int randomIndex = UnityEngine.Random.Range(0, questions.Count - 1);
currentQuestion = questions[randomIndex];
string question = currentQuestion.questionTitle;
string[] options = currentQuestion.options; // 获取选项数组
int correctIndex = currentQuestion.correctOptionIndex; // 获取正确选项的索引
titleText.text = question;
for (int i = 0; i < optionButtons.Length; i++)
{
optionButtons[i].onClick.RemoveAllListeners();
int index = i; // 创建局部变量保存索引值
optionButtons[i].GetComponentInChildren<Text>().text = options[i];
optionButtons[i].interactable = true; // 启用按钮
}
}
public void CheckAnswer(int optionIndex)
{
for (int i = 0; i < optionButtons.Length; i++)
{
optionButtons[i].interactable = false; // 禁用按钮
}
if (optionIndex == currentQuestion.correctOptionIndex)
{
Debug.Log("回答正确!" + optionIndex + currentQuestion.correctOptionIndex);
}
else
{
Debug.Log("回答错误!" + optionIndex + currentQuestion.correctOptionIndex);
}
}
}
四、收集事件
1、新增文本
(1) 新增文本:ItemsText, TipText(可设置有透明度的黑色背景)
(2) 给JohnLemon添加标签Player
2、更改Enemies下的子物体
(1) 复制PointOfView预制体,重命名为ExitEventTriggers
(2) 删除Ghost (3)的PointOfView子物体,在该位置添加ExitEventTriggers子物体
(3) 编辑PointOfView预制体,删除Observer.cs组件,添加CollectedItems.cs组件
3、新建CollectedItems.cs
(1) 设置隐藏答题面板按钮,同时调用新题目
public GameObject questionsPanel;
public Button quitBtn;
public LoadQuestions LoadQuestions;
void Start()
{
questionsPanel.SetActive(false);
}
public void HideQuestionPanel()
{
if (questionsPanel != null)
{
questionsPanel.SetActive(false); // 隐藏物体
LoadQuestions.UpdateQuestionUI();
}
}
(2) 增加显示背包物品
public Text itemsText;
static int nucleus = 0, mitochondria = 0, GolgiApparatus = 0, endoplasmicReticulum = 0, ribosome = 0;
void Start()
{
ItemsUpdate();
}
void ItemsUpdate()
{
itemsText.text = "当前持有:……" + nucleus + ";……" + mitochondria + ";……" + GolgiApparatus + ";……" + endoplasmicReticulum + ";……" + ribosome;
}
(3) 增加冷却事件
bool canTriggerEffect = true;
void RandomEvent()
{
if (canTriggerEffect)
{
//执行接触事件……
else
{
tipText.text = "你什么都没有发现……";
StartCoroutine(ClearTextRoutine(2f));//2秒后文本消失
}
canTriggerEffect = false;
StartCoroutine(CooldownRoutine());
}
}
IEnumerator CooldownRoutine()//接触事件有1分钟的冷却时间
{
yield return new WaitForSeconds(60f); // 等待1分钟
canTriggerEffect = true; // 冷却结束,可以再次触发效果
}
IEnumerator ClearTextRoutine(float delay)//提示文本显示后消失
{
yield return new WaitForSeconds(delay);
tipText.text = "";
}
(4) 增加收集事件
void Start()
{
if (itemsText == null || tipText == null)
{
Debug.LogError("ItemsText 或 TipText 未被正确分配!");
}
questionsPanel.SetActive(false);
}
private void OnTriggerEnter(Collider other)
{
if (other.CompareTag("Player"))
{
RandomEvent();
}
}
void RandomEvent()
{
int numericValue = Random.Range(0, 100);
if (numericValue < 5 && nucleus == 0)
{
nucleus = 1;
tipText.text = "你发现了nucleus!";
}
else if (numericValue < 26)
{
mitochondria++;
tipText.text = "你得到了mitochondria!";
}
else if (numericValue < 41)
{
endoplasmicReticulum++;
tipText.text = "你得到了endoplasmicReticulum*1!";
}
else if (numericValue < 56)
{
GolgiApparatus++;
tipText.text = "你得到了GolgiApparatus*1!";
}
else if (numericValue < 76)
{
ribosome++;
tipText.text = "你得到了ribosome*1!";
}
else if (numericValue < 86)
{
questionsPanel.SetActive(true);
}
else
{
tipText.text = "你什么都没有发现……";
}
ItemsUpdate();
}
void ItemsUpdate()
{
itemsText.text = "当前持有:nucleus" + nucleus + ";……" + mitochondria + ";……" + GolgiApparatus + ";……" + endoplasmicReticulum + ";……" + ribosome;
}
}
本节完整脚本
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public class CollectedItems : MonoBehaviour
{
public Text itemsText;
public Text tipText;
static int nucleus = 0, mitochondria = 0, GolgiApparatus = 0, endoplasmicReticulum = 0, ribosome = 0;
public GameObject questionsPanel;
public Button quitBtn;
public LoadQuestions LoadQuestions;
bool canTriggerEffect = true;
void Start()
{
if (itemsText == null || tipText == null)
{
Debug.LogError("ItemsText 或 TipText 未被正确分配!");
}
questionsPanel.SetActive(false);
ItemsUpdate();
}
private void OnTriggerEnter(Collider other)
{
if (other.CompareTag("Player"))
{
RandomEvent();
}
}
void RandomEvent()
{
if (canTriggerEffect)
{
int numericValue = Random.Range(0, 100);
if (numericValue < 5 && nucleus == 0)
{
nucleus = 1;
tipText.text = "你发现了nucleus!";
StartCoroutine(ClearTextRoutine(2f));
}
else if (numericValue < 26)
{
mitochondria++;
tipText.text = "你得到了mitochondria!";
StartCoroutine(ClearTextRoutine(2f));
}
else if (numericValue < 41)
{
endoplasmicReticulum++;
tipText.text = "你得到了endoplasmicReticulum*1!";
StartCoroutine(ClearTextRoutine(2f));
}
else if (numericValue < 56)
{
GolgiApparatus++;
tipText.text = "你得到了GolgiApparatus*1!";
StartCoroutine(ClearTextRoutine(2f));
}
else if (numericValue < 76)
{
ribosome++;
tipText.text = "你得到了ribosome*1!";
StartCoroutine(ClearTextRoutine(2f));
}
else if (numericValue < 86)
{
questionsPanel.SetActive(true);
}
else
{
tipText.text = "你什么都没有发现……";
StartCoroutine(ClearTextRoutine(2f));
}
ItemsUpdate();
canTriggerEffect = false;
StartCoroutine(CooldownRoutine());
}
}
void ItemsUpdate()
{
itemsText.text = "当前持有:nucleus" + nucleus + ";mitochondria" + mitochondria + ";GolgiApparatus" + GolgiApparatus + ";endoplasmicReticulum" + endoplasmicReticulum + ";ribosome" + ribosome;
}
IEnumerator CooldownRoutine()
{
yield return new WaitForSeconds(60f); // 等待1分钟
canTriggerEffect = true; // 冷却结束,可以再次触发效果
}
IEnumerator ClearTextRoutine(float delay)
{
yield return new WaitForSeconds(delay);
tipText.text = "";
}
public void HideQuestionPanel()
{
if (questionsPanel != null)
{
questionsPanel.SetActive(false); // 隐藏物体
LoadQuestions.UpdateQuestionUI();//调用新题目
}
}
}
五、增加生命系统
1、新建PlayerHealth.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class PlayerHealth : MonoBehaviour
{
public int maxHealth = 3;
private int currentHealth;
public Text life;
public GameEnding gameEndings;
private void Start()
{
currentHealth = maxHealth; // 先设置currentHealth的值
UpdateHealthText();
}
public void AddHealth()
{
if (currentHealth < maxHealth)
{
currentHealth++;
UpdateHealthText();
}
}
public void RemoveHealth()
{
if (currentHealth > 0)
{
currentHealth--;
UpdateHealthText();
if (currentHealth == 0)
{
gameEndings.CaughtPlayer(); // 玩家死亡
}
}
}
private void UpdateHealthText()
{
life.text = "当前生命" + currentHealth.ToString();
}
}
2、增加文本显示生命值
(1) 增加显示当前生命值文本
(2) 增加减少生命值提醒图片
3、处理加减生命的方法
public class LoadQuestions : MonoBehaviour
public PlayerHealth PlayerHealth;
public void CheckAnswer(int optionIndex)
{
if (optionIndex == currentQuestion.correctOptionIndex)
{
Debug.Log("回答正确!" + optionIndex + currentQuestion.correctOptionIndex);
PlayerHealth.AddHealth();
TipAnswerText.text = "回答正确!生命值+1!退出按Quit";
}
else
{
Debug.Log("回答错误!" + optionIndex + currentQuestion.correctOptionIndex);
PlayerHealth.RemoveHealth();
TipAnswerText.text = "回答错误!生命值-1!\n+退出按Quit";
}
}
六、 更改死亡事件
using UnityEngine;
using UnityEngine.SceneManagement;
public class GameEnding : MonoBehaviour
{
public float fadeDuration = 1f;
public float displayImageDuration = 1f;
public GameObject player;
public CanvasGroup exitBackgroundImageCanvasGroup;
public AudioSource exitAudio;
public CanvasGroup caughtBackgroundImageCanvasGroup;
public AudioSource caughtAudio;
bool m_IsPlayerAtExit;
bool m_IsPlayerCaught;
float m_Timer;
bool m_HasAudioPlayed;
void OnTriggerEnter (Collider other)
{
if (other.gameObject == player)
{
m_IsPlayerAtExit = true;
}
}
public void CaughtPlayer ()
{
m_IsPlayerCaught = true;
}
void Update ()
{
if (m_IsPlayerAtExit)
{
EndLevel (exitBackgroundImageCanvasGroup, false, exitAudio);
}
else if (m_IsPlayerCaught)
{
EndLevel (caughtBackgroundImageCanvasGroup, true, caughtAudio);
}
}
void EndLevel (CanvasGroup imageCanvasGroup, bool doRestart, AudioSource audioSource)
{
if (!m_HasAudioPlayed)
{
audioSource.Play();
m_HasAudioPlayed = true;
}
m_Timer += Time.deltaTime;
imageCanvasGroup.alpha = m_Timer / fadeDuration;
if (m_Timer > fadeDuration + displayImageDuration)
{
if (doRestart)
{
//SceneManager.LoadScene ("MainScene1");
Application.Quit();//退出游戏
}
else
{
Application.Quit ();
}
}
}
}
七、增加敌人,增加随机死亡事件
八、添加手柄
(1) 下载并导入 资源包
(2) 打开Prefabs 文件夹,将 Fixed Joystick 拖放到Hierarchy面板的Canvas下,调整大小和位置
(3) 调整脚本
public FixedJoystick fixedJoystick;
public float moveSpeed = 1f;
void Start()
{
……
fixedJoystick = GameObject.FindObjectOfType<FixedJoystick>();
}
private void FixedUpdate()
{
// 手柄控制代码
if (fixedJoystick != null)
{
float joystickHorizontal = fixedJoystick.Horizontal;
float joystickVertical = fixedJoystick.Vertical;
if (!Mathf.Approximately(joystickHorizontal, 0f) || !Mathf.Approximately(joystickVertical, 0f))
{
horizontal = joystickHorizontal;
vertical = joystickVertical;
m_Movement.Set(horizontal, 0f, vertical);
m_Movement.Normalize();
}
}
}
完整代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
public class PlayerMovement : MonoBehaviour
{
public FixedJoystick fixedJoystick;
public InputAction MoveAction;
public float turnSpeed = 20f;
public float moveSpeed = 1f;
Animator m_Animator;
Rigidbody m_Rigidbody;
AudioSource m_AudioSource;
Vector3 m_Movement;
Quaternion m_Rotation = Quaternion.identity;
void Start ()
{
m_Animator = GetComponent<Animator> ();
m_Rigidbody = GetComponent<Rigidbody> ();
m_AudioSource = GetComponent<AudioSource> ();
MoveAction.Enable();
fixedJoystick = GameObject.FindObjectOfType<FixedJoystick>();
}
void FixedUpdate()
{
// 原有的键盘控制代码
float horizontal = Input.GetAxis("Horizontal");
float vertical = Input.GetAxis("Vertical");
m_Movement.Set(horizontal, 0f, vertical);
m_Movement.Normalize();
// 手柄控制代码
if (fixedJoystick != null)
{
float joystickHorizontal = fixedJoystick.Horizontal;
float joystickVertical = fixedJoystick.Vertical;
if (!Mathf.Approximately(joystickHorizontal, 0f) || !Mathf.Approximately(joystickVertical, 0f))
{
horizontal = joystickHorizontal;
vertical = joystickVertical;
m_Movement.Set(horizontal, 0f, vertical);
m_Movement.Normalize();
}
}
bool hasHorizontalInput = !Mathf.Approximately(horizontal, 0f);
bool hasVerticalInput = !Mathf.Approximately(vertical, 0f);
bool isWalking = hasHorizontalInput || hasVerticalInput;
m_Animator.SetBool("IsWalking", isWalking);
if (isWalking)
{
if (!m_AudioSource.isPlaying)
{
m_AudioSource.Play();
}
}
else
{
m_AudioSource.Stop();
}
Vector3 desiredForward = Vector3.RotateTowards(transform.forward, m_Movement, turnSpeed * Time.deltaTime, 0f);
m_Rotation = Quaternion.LookRotation(desiredForward);
// 移动角色
m_Rigidbody.MovePosition(m_Rigidbody.position + m_Movement * moveSpeed * Time.fixedDeltaTime);
}
void OnAnimatorMove ()
{
m_Rigidbody.MovePosition (m_Rigidbody.position + m_Movement * m_Animator.deltaPosition.magnitude);
m_Rigidbody.MoveRotation (m_Rotation);
}
}
九、发布
1、安装 WebGL Build Support
2、更改Color Space(other……)下
3、在File下进行打包
4、在publish中上传