feat: VFS 虚拟仿真实验核心框架 v2
- 加权评分系统:每步独立 ScoreWeight,满分 100,SequenceClick 按子项比例计分 - StepData 新增 DetailedContent(学习模式详细操作说明)和 SubStepCount - 伤情检查步骤扩展为 5 子项序列(视觉检查→肿胀→疼痛→生命体征→神经评估) - 夹板固定模式完整评分(选择夹板→放置衬垫→夹板固定→固定后检查) - Doc/ 文件夹归档三份文档:项目大纲、文案、评分标准 - README 更新评分权重表和夹板固定流程
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
using System;
|
||||
|
||||
/// <summary>
|
||||
/// 考核弹窗:选择题 + 操作确认。
|
||||
/// 挂在弹窗 Panel 上,由 UIManager 调用 Show()。
|
||||
/// 回调返回 proportion (0~1 得分比例) 和 feedback。
|
||||
/// </summary>
|
||||
public class ExamPopup : MonoBehaviour
|
||||
{
|
||||
[Header("UI 组件")]
|
||||
[SerializeField] private GameObject _popupRoot;
|
||||
[SerializeField] private TMP_Text _questionText;
|
||||
[SerializeField] private Toggle[] _optionToggles;
|
||||
[SerializeField] private TMP_Text[] _optionLabels;
|
||||
[SerializeField] private Button _confirmButton;
|
||||
[SerializeField] private TMP_Text _feedbackText;
|
||||
|
||||
[Header("操作题 UI(点击序列)")]
|
||||
[SerializeField] private GameObject _operationPanel;
|
||||
[SerializeField] private TMP_Text _operationHint;
|
||||
|
||||
private StepData _currentStep;
|
||||
private Action<float, string> _onAnswerCallback;
|
||||
private int _selectedIndex = -1;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_confirmButton?.onClick.AddListener(OnConfirm);
|
||||
if (_popupRoot != null) _popupRoot.SetActive(false);
|
||||
}
|
||||
|
||||
public void Show(StepData step, Action<float, string> onAnswer)
|
||||
{
|
||||
_currentStep = step;
|
||||
_onAnswerCallback = onAnswer;
|
||||
_selectedIndex = -1;
|
||||
if (_feedbackText != null) _feedbackText.text = "";
|
||||
if (_popupRoot != null) _popupRoot.SetActive(true);
|
||||
|
||||
_questionText?.SetText(step.ExamQuestionText);
|
||||
|
||||
switch (step.CompleteType)
|
||||
{
|
||||
case StepCompleteType.MultipleChoice:
|
||||
SetupMultipleChoice(step);
|
||||
break;
|
||||
case StepCompleteType.ClickObject:
|
||||
SetupClickObject(step);
|
||||
break;
|
||||
case StepCompleteType.SequenceClick:
|
||||
SetupSequenceClick(step);
|
||||
break;
|
||||
default:
|
||||
SetupDefault(step);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#region 题型配置
|
||||
|
||||
private void SetupMultipleChoice(StepData step)
|
||||
{
|
||||
if (_operationPanel != null) _operationPanel.SetActive(false);
|
||||
|
||||
int optionCount = step.ExamOptions?.Length ?? 0;
|
||||
for (int i = 0; i < _optionToggles.Length; i++)
|
||||
{
|
||||
bool active = i < optionCount;
|
||||
_optionToggles[i].gameObject.SetActive(active);
|
||||
_optionToggles[i].isOn = false;
|
||||
_optionToggles[i].onValueChanged.RemoveAllListeners();
|
||||
int idx = i;
|
||||
_optionToggles[i].onValueChanged.AddListener((val) => { if (val) _selectedIndex = idx; });
|
||||
if (active) _optionLabels[i]?.SetText(step.ExamOptions[i]);
|
||||
}
|
||||
|
||||
_confirmButton?.gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
private void SetupClickObject(StepData step)
|
||||
{
|
||||
if (_operationPanel != null) _operationPanel.SetActive(true);
|
||||
if (_optionToggles != null)
|
||||
foreach (var t in _optionToggles) t.gameObject.SetActive(false);
|
||||
_confirmButton?.gameObject.SetActive(true);
|
||||
_operationHint?.SetText($"请点击场景中的 {step.TargetTag} 物体");
|
||||
}
|
||||
|
||||
private void SetupSequenceClick(StepData step)
|
||||
{
|
||||
if (_operationPanel != null) _operationPanel.SetActive(true);
|
||||
if (_optionToggles != null)
|
||||
foreach (var t in _optionToggles) t.gameObject.SetActive(false);
|
||||
_confirmButton?.gameObject.SetActive(true);
|
||||
_operationHint?.SetText("请按正确顺序点击物体,完成后按确定");
|
||||
}
|
||||
|
||||
private void SetupDefault(StepData step)
|
||||
{
|
||||
if (_operationPanel != null) _operationPanel.SetActive(false);
|
||||
if (_optionToggles != null)
|
||||
foreach (var t in _optionToggles) t.gameObject.SetActive(false);
|
||||
_confirmButton?.gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 确认与判定
|
||||
|
||||
private void OnConfirm()
|
||||
{
|
||||
if (_currentStep == null) return;
|
||||
|
||||
float proportion = 1f;
|
||||
string feedback = "";
|
||||
|
||||
switch (_currentStep.CompleteType)
|
||||
{
|
||||
case StepCompleteType.MultipleChoice:
|
||||
bool correct = _selectedIndex == _currentStep.CorrectOptionIndex;
|
||||
proportion = correct ? 1f : 0f;
|
||||
feedback = correct ? "" : _currentStep.ErrorFeedback;
|
||||
if (!correct && _feedbackText != null)
|
||||
_feedbackText.text = feedback;
|
||||
break;
|
||||
default:
|
||||
proportion = 1f;
|
||||
break;
|
||||
}
|
||||
|
||||
if (proportion <= 0f && !string.IsNullOrEmpty(feedback))
|
||||
{
|
||||
_onAnswerCallback?.Invoke(0f, feedback);
|
||||
Hide();
|
||||
}
|
||||
else
|
||||
{
|
||||
_onAnswerCallback?.Invoke(proportion, feedback);
|
||||
Hide();
|
||||
}
|
||||
}
|
||||
|
||||
public void Hide()
|
||||
{
|
||||
if (_popupRoot != null) _popupRoot.SetActive(false);
|
||||
_currentStep = null;
|
||||
_onAnswerCallback = null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
|
||||
/// <summary>
|
||||
/// 主菜单:实验类型选择 + 模式选择 + 固定方式选择。
|
||||
/// 挂在主菜单场景的 Canvas 上。
|
||||
/// </summary>
|
||||
public class MainMenu : MonoBehaviour
|
||||
{
|
||||
[Header("主标题")]
|
||||
[SerializeField] private TMP_Text _titleText;
|
||||
|
||||
[Header("实验选择按钮 - 底部图标")]
|
||||
[SerializeField] private Button _btnCPR;
|
||||
[SerializeField] private Button _btnFracture;
|
||||
[SerializeField] private Button _btnSprain;
|
||||
[SerializeField] private Button _btnBleeding;
|
||||
[SerializeField] private Button _btnTransport;
|
||||
|
||||
[Header("模式选择")]
|
||||
[SerializeField] private TMP_Text _tipText;
|
||||
[SerializeField] private Button _btnLearnMode;
|
||||
[SerializeField] private Button _btnExamMode;
|
||||
[SerializeField] private TMP_Text _selectedModeLabel;
|
||||
|
||||
[Header("固定方式选择(仅骨折固定)")]
|
||||
[SerializeField] private GameObject _fixationPanel;
|
||||
[SerializeField] private Button _btnLimbFixation;
|
||||
[SerializeField] private Button _btnSplintFixation;
|
||||
[SerializeField] private TMP_Text _selectedFixationLabel;
|
||||
|
||||
[Header("开始按钮")]
|
||||
[SerializeField] private Button _btnStart;
|
||||
|
||||
[Header("提示")]
|
||||
[SerializeField] private TMP_Text _selectionTip;
|
||||
|
||||
// 当前选择
|
||||
private ExperimentType _selectedType = ExperimentType.FractureFixation;
|
||||
private ExperimentMode _selectedMode = ExperimentMode.Learn;
|
||||
private FixationMethod _selectedFixation = FixationMethod.LimbFixation;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
BindExperimentButtons();
|
||||
_btnLearnMode?.onClick.AddListener(() => SelectMode(ExperimentMode.Learn));
|
||||
_btnExamMode?.onClick.AddListener(() => SelectMode(ExperimentMode.Exam));
|
||||
_btnLimbFixation?.onClick.AddListener(() => SelectFixation(FixationMethod.LimbFixation));
|
||||
_btnSplintFixation?.onClick.AddListener(() => SelectFixation(FixationMethod.SplintFixation));
|
||||
_btnStart?.onClick.AddListener(StartExperiment);
|
||||
|
||||
// 默认选中骨折固定
|
||||
SelectExperiment(ExperimentType.FractureFixation);
|
||||
SelectMode(ExperimentMode.Learn);
|
||||
SelectFixation(FixationMethod.LimbFixation);
|
||||
}
|
||||
|
||||
private void BindExperimentButtons()
|
||||
{
|
||||
_btnCPR?.onClick.AddListener(() => SelectExperiment(ExperimentType.CPR));
|
||||
_btnFracture?.onClick.AddListener(() => SelectExperiment(ExperimentType.FractureFixation));
|
||||
_btnSprain?.onClick.AddListener(() => SelectExperiment(ExperimentType.SprainBandaging));
|
||||
_btnBleeding?.onClick.AddListener(() => SelectExperiment(ExperimentType.BleedingBandaging));
|
||||
_btnTransport?.onClick.AddListener(() => SelectExperiment(ExperimentType.CasualtyTransport));
|
||||
}
|
||||
|
||||
private void SelectExperiment(ExperimentType type)
|
||||
{
|
||||
_selectedType = type;
|
||||
// 骨折固定显示固定方式面板
|
||||
bool isFracture = type == ExperimentType.FractureFixation;
|
||||
if (_fixationPanel != null) _fixationPanel.SetActive(isFracture);
|
||||
if (_selectionTip != null)
|
||||
_selectionTip.text = isFracture ? "Tip:请选择固定方式" : "";
|
||||
UpdateStartButton();
|
||||
}
|
||||
|
||||
private void SelectMode(ExperimentMode mode)
|
||||
{
|
||||
_selectedMode = mode;
|
||||
if (_selectedModeLabel != null)
|
||||
_selectedModeLabel.text = mode == ExperimentMode.Learn ? "学习模式" : "考核模式";
|
||||
UpdateStartButton();
|
||||
}
|
||||
|
||||
private void SelectFixation(FixationMethod fixation)
|
||||
{
|
||||
_selectedFixation = fixation;
|
||||
if (_selectedFixationLabel != null)
|
||||
_selectedFixationLabel.text = fixation == FixationMethod.LimbFixation ? "肢体固定" : "夹板固定";
|
||||
UpdateStartButton();
|
||||
}
|
||||
|
||||
private void UpdateStartButton()
|
||||
{
|
||||
if (_btnStart == null) return;
|
||||
_btnStart.interactable = true;
|
||||
}
|
||||
|
||||
private void StartExperiment()
|
||||
{
|
||||
var gm = GameManager.Instance;
|
||||
if (gm == null)
|
||||
{
|
||||
Debug.LogError("MainMenu: GameManager.Instance 为空!请确保场景中有 GameManager。");
|
||||
return;
|
||||
}
|
||||
|
||||
gm.CurrentExperimentType = _selectedType;
|
||||
gm.CurrentMode = _selectedMode;
|
||||
gm.CurrentFixationMethod = _selectedFixation;
|
||||
|
||||
// 加载实验场景(所有实验共用一个场景,StepManager 根据配置区分)
|
||||
UnityEngine.SceneManagement.SceneManager.LoadScene("Experiment");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
|
||||
/// <summary>
|
||||
/// 实验结束后的结果展示面板。
|
||||
/// 考核模式:显示加权成绩 + 错题分析列表(含每步得分/满分)。
|
||||
/// 学习模式:显示注意事项。
|
||||
/// </summary>
|
||||
public class ScoreDisplay : MonoBehaviour
|
||||
{
|
||||
[Header("面板根节点")]
|
||||
[SerializeField] private GameObject _panelRoot;
|
||||
|
||||
[Header("考核模式")]
|
||||
[SerializeField] private GameObject _examResultRoot;
|
||||
[SerializeField] private TMP_Text _scoreTitle;
|
||||
[SerializeField] private TMP_Text _scoreValue;
|
||||
[SerializeField] private Transform _analysisListParent;
|
||||
[SerializeField] private GameObject _analysisItemPrefab;
|
||||
|
||||
[Header("学习模式 - 注意事项")]
|
||||
[SerializeField] private GameObject _notesRoot;
|
||||
[SerializeField] private TMP_Text _notesText;
|
||||
|
||||
[Header("底部按钮")]
|
||||
[SerializeField] private Button _confirmButton;
|
||||
|
||||
private static readonly string NotesContent =
|
||||
"1. 下肢损伤选择仰卧位;\n" +
|
||||
"2. 骨折固定前后要脱掉鞋袜并检查下肢末梢循环、运动、感觉;\n" +
|
||||
"3. 穿带子时应从健侧穿向伤侧;\n" +
|
||||
"4. 骨折固定时先固定伤口的近心端,后固定远心端,再由上而下进行固定;\n" +
|
||||
"5. 固定后尽可能抬高下肢。";
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_confirmButton?.onClick.AddListener(OnConfirm);
|
||||
if (_panelRoot != null) _panelRoot.SetActive(false);
|
||||
}
|
||||
|
||||
/// <summary>考核模式 - 显示加权成绩</summary>
|
||||
public void ShowExamScore()
|
||||
{
|
||||
if (_panelRoot != null) _panelRoot.SetActive(true);
|
||||
if (_examResultRoot != null) _examResultRoot.SetActive(true);
|
||||
if (_notesRoot != null) _notesRoot.SetActive(false);
|
||||
|
||||
var gm = GameManager.Instance;
|
||||
if (gm == null) return;
|
||||
|
||||
int score = gm.GetExamScoreRounded();
|
||||
float pct = gm.GetExamPercentage();
|
||||
if (_scoreTitle != null) _scoreTitle.text = "成绩显示栏";
|
||||
if (_scoreValue != null) _scoreValue.text = $"考试成绩:{score} 分({pct:F0}%)";
|
||||
|
||||
BuildAnalysisList();
|
||||
}
|
||||
|
||||
/// <summary>学习模式 - 显示注意事项</summary>
|
||||
public void ShowNotesOnly()
|
||||
{
|
||||
if (_panelRoot != null) _panelRoot.SetActive(true);
|
||||
if (_examResultRoot != null) _examResultRoot.SetActive(false);
|
||||
if (_notesRoot != null) _notesRoot.SetActive(true);
|
||||
|
||||
if (_notesText != null) _notesText.text = NotesContent;
|
||||
}
|
||||
|
||||
private void BuildAnalysisList()
|
||||
{
|
||||
if (_analysisListParent == null || _analysisItemPrefab == null) return;
|
||||
|
||||
foreach (Transform child in _analysisListParent)
|
||||
Destroy(child.gameObject);
|
||||
|
||||
var gm = GameManager.Instance;
|
||||
if (gm == null || gm.StepScores == null) return;
|
||||
|
||||
for (int i = 0; i < gm.StepScores.Length; i++)
|
||||
{
|
||||
var item = Instantiate(_analysisItemPrefab, _analysisListParent);
|
||||
var text = item.GetComponentInChildren<TMP_Text>();
|
||||
if (text == null) continue;
|
||||
|
||||
float earned = gm.StepScores[i];
|
||||
float max = gm.StepMaxScores[i];
|
||||
bool fullScore = Mathf.Approximately(earned, max);
|
||||
bool zeroScore = earned <= 0f;
|
||||
|
||||
string status;
|
||||
if (fullScore) status = "<color=#00FF00>正确</color>";
|
||||
else if (zeroScore) status = "<color=#FF0000>错误</color>";
|
||||
else status = $"<color=#FFAA00>部分正确</color>";
|
||||
|
||||
string scorePart = $"({earned:F0}/{max:F0} 分)";
|
||||
string fbPart = (!fullScore && !string.IsNullOrEmpty(gm.StepFeedback[i]))
|
||||
? $":{gm.StepFeedback[i]}" : "";
|
||||
|
||||
text.text = $"第{i + 1}题:{status} {scorePart}{fbPart}";
|
||||
}
|
||||
}
|
||||
|
||||
private void OnConfirm()
|
||||
{
|
||||
if (_panelRoot != null) _panelRoot.SetActive(false);
|
||||
UnityEngine.SceneManagement.SceneManager.LoadScene("MainMenu");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
|
||||
/// <summary>
|
||||
/// UI 中央调度器:引用所有子面板,绑定 StepManager 事件。
|
||||
/// 挂在 Canvas GameObject 上。
|
||||
/// </summary>
|
||||
public class UIManager : MonoBehaviour
|
||||
{
|
||||
[Header("管理器引用")]
|
||||
[SerializeField] private StepManager _stepManager;
|
||||
[SerializeField] private InteractionSystem _interactionSystem;
|
||||
|
||||
[Header("=== 顶部提示栏 ===")]
|
||||
[SerializeField] private GameObject _tipPanel;
|
||||
[SerializeField] private TMP_Text _tipText;
|
||||
|
||||
[Header("=== 详细操作说明(甲方补充 - 学习模式) ===")]
|
||||
[SerializeField] private GameObject _detailPanel;
|
||||
[SerializeField] private TMP_Text _detailText;
|
||||
|
||||
[Header("=== 对话气泡 ===")]
|
||||
[SerializeField] private GameObject _dialogueBubble;
|
||||
[SerializeField] private TMP_Text _dialogueText;
|
||||
|
||||
[Header("=== 画中画视频 ===")]
|
||||
[SerializeField] private VideoController _videoController;
|
||||
|
||||
[Header("=== 右侧功能按钮 ===")]
|
||||
[SerializeField] private Button _btnFullScreen;
|
||||
[SerializeField] private Button _btnPrevious;
|
||||
[SerializeField] private Button _btnNext;
|
||||
[SerializeField] private Button _btnSwitchMode;
|
||||
[SerializeField] private Button _btnReturn;
|
||||
[SerializeField] private TMP_Text _btnSwitchModeLabel;
|
||||
|
||||
[Header("=== 底部进度条 ===")]
|
||||
[SerializeField] private Slider _progressBar;
|
||||
[SerializeField] private TMP_Text _progressText;
|
||||
|
||||
[Header("=== 弹窗 ===")]
|
||||
[SerializeField] private ExamPopup _examPopup;
|
||||
[SerializeField] private ScoreDisplay _scoreDisplay;
|
||||
|
||||
[Header("=== 底部主菜单图标 ===")]
|
||||
[SerializeField] private Button _btnCPR;
|
||||
[SerializeField] private Button _btnFracture;
|
||||
[SerializeField] private Button _btnSprain;
|
||||
[SerializeField] private Button _btnBleeding;
|
||||
[SerializeField] private Button _btnTransport;
|
||||
|
||||
[Header("=== 固定方式选择(仅骨折固定) ===")]
|
||||
[SerializeField] private GameObject _fixationPanel;
|
||||
[SerializeField] private Button _btnLimbFixation;
|
||||
[SerializeField] private Button _btnSplintFixation;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
BindEvents();
|
||||
BindButtons();
|
||||
}
|
||||
|
||||
#region 事件绑定
|
||||
|
||||
private void BindEvents()
|
||||
{
|
||||
if (_stepManager == null) return;
|
||||
|
||||
_stepManager.OnStepChanged.AddListener(OnStepChanged);
|
||||
_stepManager.OnTipChanged.AddListener(UpdateTip);
|
||||
_stepManager.OnDialogueChanged.AddListener(UpdateDialogue);
|
||||
_stepManager.OnDetailedContent.AddListener(UpdateDetailContent);
|
||||
_stepManager.OnExamPopup.AddListener(ShowExamPopup);
|
||||
_stepManager.OnExperimentComplete.AddListener(OnExperimentComplete);
|
||||
_stepManager.OnVideoToggle.AddListener(ToggleVideo);
|
||||
}
|
||||
|
||||
private void BindButtons()
|
||||
{
|
||||
_btnFullScreen?.onClick.AddListener(ToggleFullScreen);
|
||||
_btnPrevious?.onClick.AddListener(OnPreviousClicked);
|
||||
_btnNext?.onClick.AddListener(OnNextClicked);
|
||||
_btnSwitchMode?.onClick.AddListener(OnSwitchModeClicked);
|
||||
_btnReturn?.onClick.AddListener(OnReturnClicked);
|
||||
|
||||
_btnLimbFixation?.onClick.AddListener(() => _stepManager?.SwitchFixation(FixationMethod.LimbFixation));
|
||||
_btnSplintFixation?.onClick.AddListener(() => _stepManager?.SwitchFixation(FixationMethod.SplintFixation));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region UI 更新回调
|
||||
|
||||
private void OnStepChanged(int index)
|
||||
{
|
||||
UpdateProgressBar();
|
||||
UpdateNavigationButtons();
|
||||
}
|
||||
|
||||
private void UpdateTip(string text)
|
||||
{
|
||||
if (_tipPanel != null) _tipPanel.SetActive(!string.IsNullOrEmpty(text));
|
||||
if (_tipText != null) _tipText.text = text;
|
||||
}
|
||||
|
||||
private void UpdateDialogue(string text)
|
||||
{
|
||||
if (_dialogueBubble != null) _dialogueBubble.SetActive(!string.IsNullOrEmpty(text));
|
||||
if (_dialogueText != null) _dialogueText.text = text;
|
||||
}
|
||||
|
||||
private void UpdateDetailContent(string text)
|
||||
{
|
||||
if (_detailPanel != null) _detailPanel.SetActive(!string.IsNullOrEmpty(text));
|
||||
if (_detailText != null) _detailText.text = text;
|
||||
}
|
||||
|
||||
private void ToggleVideo(string fileName, bool active)
|
||||
{
|
||||
if (_videoController == null) return;
|
||||
if (active && !string.IsNullOrEmpty(fileName))
|
||||
_videoController.PlayVideo(fileName);
|
||||
else
|
||||
_videoController.StopVideo();
|
||||
}
|
||||
|
||||
private void UpdateProgressBar()
|
||||
{
|
||||
if (_stepManager == null) return;
|
||||
int cur = _stepManager.CurrentStepIndex + 1;
|
||||
int total = _stepManager.TotalSteps;
|
||||
if (_progressBar != null) _progressBar.value = (float)cur / total;
|
||||
if (_progressText != null) _progressText.text = $"{cur}/{total}";
|
||||
}
|
||||
|
||||
private void UpdateNavigationButtons()
|
||||
{
|
||||
if (_stepManager == null) return;
|
||||
_btnPrevious.interactable = _stepManager.CurrentStepIndex > 0;
|
||||
bool isLast = _stepManager.CurrentStepIndex >= _stepManager.TotalSteps - 1;
|
||||
if (_btnNext != null)
|
||||
{
|
||||
var label = _btnNext.GetComponentInChildren<TMP_Text>();
|
||||
if (label != null) label.text = isLast ? "完成" : "下一步";
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 考核弹窗
|
||||
|
||||
private void ShowExamPopup(StepData step)
|
||||
{
|
||||
if (_examPopup == null) return;
|
||||
_examPopup.Show(step, OnExamAnswer);
|
||||
}
|
||||
|
||||
private void OnExamAnswer(float proportion, string feedback)
|
||||
{
|
||||
_stepManager?.OnStepInteractionComplete(proportion, feedback);
|
||||
_stepManager?.NextStep();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 实验完成
|
||||
|
||||
private void OnExperimentComplete()
|
||||
{
|
||||
var gm = GameManager.Instance;
|
||||
if (gm == null) return;
|
||||
if (gm.CurrentMode == ExperimentMode.Exam)
|
||||
{
|
||||
_scoreDisplay?.ShowExamScore();
|
||||
}
|
||||
else
|
||||
{
|
||||
_scoreDisplay?.ShowNotesOnly();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 按钮回调
|
||||
|
||||
private void ToggleFullScreen()
|
||||
{
|
||||
Screen.fullScreen = !Screen.fullScreen;
|
||||
}
|
||||
|
||||
private void OnPreviousClicked()
|
||||
{
|
||||
_stepManager?.PreviousStep();
|
||||
}
|
||||
|
||||
private void OnNextClicked()
|
||||
{
|
||||
if (_stepManager.Mode == ExperimentMode.Exam)
|
||||
{
|
||||
var step = _stepManager.GetCurrentStep();
|
||||
if (step != null && step.CompleteType != StepCompleteType.MultipleChoice)
|
||||
_stepManager.OnStepInteractionComplete(1f);
|
||||
}
|
||||
_stepManager.NextStep();
|
||||
}
|
||||
|
||||
private void OnSwitchModeClicked()
|
||||
{
|
||||
var newMode = _stepManager.Mode == ExperimentMode.Learn
|
||||
? ExperimentMode.Exam : ExperimentMode.Learn;
|
||||
_stepManager.SwitchMode(newMode);
|
||||
}
|
||||
|
||||
private void OnReturnClicked()
|
||||
{
|
||||
UnityEngine.SceneManagement.SceneManager.LoadScene("MainMenu");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityEngine.Video;
|
||||
|
||||
/// <summary>
|
||||
/// 画中画视频控制器:用 VideoPlayer 渲染到 RenderTexture,再赋给 RawImage。
|
||||
/// 挂在包含 RawImage 和 VideoPlayer 的 GameObject 上。
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(RawImage))]
|
||||
public class VideoController : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private VideoPlayer _videoPlayer;
|
||||
[SerializeField] private RawImage _rawImage;
|
||||
|
||||
private RenderTexture _renderTexture;
|
||||
private bool _isPlaying;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (_rawImage == null) _rawImage = GetComponent<RawImage>();
|
||||
if (_videoPlayer == null) _videoPlayer = GetComponentInChildren<VideoPlayer>();
|
||||
|
||||
if (_videoPlayer != null)
|
||||
{
|
||||
_videoPlayer.loopPointReached += OnVideoEnd;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>播放指定视频文件(StreamingAssets/Videos/ 下)</summary>
|
||||
public void PlayVideo(string fileName)
|
||||
{
|
||||
if (_videoPlayer == null) return;
|
||||
|
||||
string path = System.IO.Path.Combine(Application.streamingAssetsPath, "Videos", fileName);
|
||||
_videoPlayer.url = path;
|
||||
|
||||
// 创建 RenderTexture
|
||||
if (_renderTexture == null)
|
||||
{
|
||||
int w = _videoPlayer.targetTexture != null ? _videoPlayer.targetTexture.width : 640;
|
||||
int h = _videoPlayer.targetTexture != null ? _videoPlayer.targetTexture.height : 360;
|
||||
_renderTexture = new RenderTexture(w, h, 0);
|
||||
_videoPlayer.targetTexture = _renderTexture;
|
||||
if (_rawImage != null) _rawImage.texture = _renderTexture;
|
||||
}
|
||||
|
||||
_videoPlayer.Play();
|
||||
_isPlaying = true;
|
||||
gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
public void StopVideo()
|
||||
{
|
||||
if (_videoPlayer != null) _videoPlayer.Stop();
|
||||
_isPlaying = false;
|
||||
gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
public void PauseVideo()
|
||||
{
|
||||
if (_videoPlayer != null) _videoPlayer.Pause();
|
||||
}
|
||||
|
||||
public void ResumeVideo()
|
||||
{
|
||||
if (_videoPlayer != null) _videoPlayer.Play();
|
||||
}
|
||||
|
||||
private void OnVideoEnd(VideoPlayer vp)
|
||||
{
|
||||
// 真人演示视频播完自动循环
|
||||
vp.Play();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (_renderTexture != null)
|
||||
{
|
||||
_renderTexture.Release();
|
||||
Destroy(_renderTexture);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user