feat: MCP Unity 场景优化 — 完整交互体系搭建

- 删除重复 Ground
- 创建 [Managers] 空物体,挂载 GameManager + StepManager + ExperimentFlowCoordinator + AudioManager
- Main Camera 挂载 InteractionSystem
- 伤者 InjectedPerson 下创建 5 个交互子物体 (InjuryVisual/Swelling/Pain/VitalSigns/Neuro)
- 创建绑带位置、踝关节、衬垫、夹板位置等空物体
- 道具摆放到位:伤者(2,0.05,5) 施救者(3,0,5) 鞋(2.7,0.15,5.5) 绷带/夹板/急救箱在旁
- InjuryLeg Tag 修正,Ground Tag 修正
- 全部新脚本同步导入 (ExperimentTypes/StepData/ExperimentConfig/StepManager/GameManager/ExperimentFlowCoordinator/InteractionSystem/DraggableObject/HighlightEffect/UIManager/ExamPopup/ScoreDisplay/VideoController/AudioManager/CharacterAnimController/StepAnimationHandler)
- 旧脚本 (ExperimentController/ConfigLoader/AnimationController/HUDController/VideoPlayerController/PopupSystem/ModeSelectionUI) 全部兼容修复或 stub
- 编译通过,0 错误
- Canvas 挂载 UIManager
This commit is contained in:
yangbear
2026-06-24 02:33:48 +08:00
parent 23d3d282f8
commit f0a6b96da5
111 changed files with 11023 additions and 440 deletions
+3
View File
@@ -0,0 +1,3 @@
using UnityEngine;
/// <summary>Legacy — replaced by ExperimentConfig ScriptableObject.</summary>
public class ConfigLoader : MonoBehaviour { }
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bda8b3bd9c4a14051b7821859e6095f6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,3 @@
using UnityEngine;
/// <summary>Legacy — replaced by ExperimentFlowCoordinator.</summary>
public class ExperimentController : MonoBehaviour { }
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5d499f2451702432ebbf25bfccbe1fa9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,100 @@
using UnityEngine;
/// <summary>
/// 实验场景的总协调器:挂载在 Experiment 场景的根 GameObject 上,
/// 负责将 StepManager / InteractionSystem / UIManager 串联在一起。
/// 考核模式以 proportion0~1)计分。
/// </summary>
public class ExperimentFlowCoordinator : MonoBehaviour
{
[Header("核心组件引用")]
[SerializeField] private StepManager _stepManager;
[SerializeField] private InteractionSystem _interactionSystem;
[SerializeField] private UIManager _uiManager;
[SerializeField] private StepAnimationHandler _animationHandler;
[SerializeField] private AudioManager _audioManager;
[Header("配置")]
[SerializeField] private ExperimentConfig _defaultConfig;
private void Start()
{
if (_interactionSystem != null && _stepManager != null)
{
_interactionSystem.OnObjectClicked.AddListener(OnObjectClicked);
_interactionSystem.OnAnywhereClicked.AddListener(OnAnywhereClicked);
}
if (_stepManager != null && _animationHandler != null)
{
_stepManager.OnStepChanged.AddListener(_animationHandler.PlayStepAnimation);
}
if (_defaultConfig != null)
{
_stepManager.Config = _defaultConfig;
}
_stepManager.InitializeFromGameManager();
}
#region
private void OnObjectClicked(string tag)
{
var step = _stepManager.GetCurrentStep();
if (step == null) return;
switch (step.CompleteType)
{
case StepCompleteType.ClickObject:
if (tag == step.TargetTag)
{
Debug.Log($"ExperimentFlowCoordinator: 正确点击了 {tag}");
_stepManager.OnStepInteractionComplete(1f);
}
else
{
Debug.Log($"ExperimentFlowCoordinator: 点击了 {tag},需要的是 {step.TargetTag}");
if (_stepManager.Mode == ExperimentMode.Exam)
_stepManager.OnStepInteractionComplete(0f, "点击位置不正确");
}
break;
case StepCompleteType.SequenceClick:
// 按子项命中比例计分
bool isComplete;
bool isCorrect = _interactionSystem.TrySequenceClick(tag, out isComplete);
if (isComplete)
{
// 取当前序列已命中数 / 总子项数
int hit = _interactionSystem.GetSequenceHitCount();
int total = step.ClickSequenceTags?.Length ?? step.SubStepCount;
float proportion = total > 0 ? (float)hit / total : 1f;
string fb = proportion < 1f ? $"顺序部分正确({hit}/{total} 步正确)" : "";
_stepManager.OnStepInteractionComplete(proportion, fb);
}
else if (!isCorrect)
{
_stepManager.OnStepInteractionComplete(0f, "点击顺序不正确");
}
break;
default:
break;
}
}
private void OnAnywhereClicked()
{
var step = _stepManager.GetCurrentStep();
if (step == null) return;
if (step.CompleteType == StepCompleteType.ClickAnywhere)
{
_stepManager.OnStepInteractionComplete(1f);
}
}
#endregion
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e3dd6cb15ae1a4f0e92e2826f64ee6cf
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+38
View File
@@ -0,0 +1,38 @@
// ============================================================
// 实验类型、模式、固定方式的枚举定义
// ============================================================
/// <summary>五大急救实验类型</summary>
public enum ExperimentType
{
CPR, // 心肺复苏
FractureFixation, // 骨折固定
SprainBandaging, // 扭伤包扎
BleedingBandaging, // 出血包扎
CasualtyTransport // 伤员搬运
}
/// <summary>运行模式</summary>
public enum ExperimentMode
{
Learn, // 学习模式
Exam // 考核模式
}
/// <summary>骨折固定方式(仅骨折固定实验使用)</summary>
public enum FixationMethod
{
LimbFixation, // 肢体固定
SplintFixation // 夹板固定
}
/// <summary>单个步骤完成的判定类型</summary>
public enum StepCompleteType
{
None, // 无需交互,自动进入下一步
ClickObject, // 点击指定 3D 物体
DragToTarget, // 拖拽物体到目标位置
ClickAnywhere, // 点击任意空白处
MultipleChoice, // 选择题(考核模式)
SequenceClick // 按顺序点击多个物体(考核模式)
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 13ab85c5e00ee4765918090eeac6de36
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+75
View File
@@ -0,0 +1,75 @@
using UnityEngine;
/// <summary>
/// 全局单例管理器:持有当前实验的类型、模式、固定方式与加权分数。
/// 挂在场景第一个 GameObject 上,设置 DontDestroyOnLoad。
/// </summary>
public class GameManager : MonoBehaviour
{
public static GameManager Instance { get; private set; }
[Header("当前实验设置(运行时赋值)")]
public ExperimentType CurrentExperimentType = ExperimentType.FractureFixation;
public ExperimentMode CurrentMode = ExperimentMode.Learn;
public FixationMethod CurrentFixationMethod = FixationMethod.LimbFixation;
[Header("加权评分")]
public float TotalWeightedScore; // 实际得分(加权累加)
public float MaxWeightedScore = 100f; // 总分
[Header("考核记录")]
public float[] StepScores; // 每步实际得分(含权重)
public float[] StepMaxScores; // 每步满分(权重值)
public string[] StepFeedback; // 每题错误反馈文本
public bool[] StepCorrect; // 每题是否有任何得分
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
}
/// <summary>初始化考核记录数组</summary>
public void InitExamRecord(int stepCount)
{
StepScores = new float[stepCount];
StepMaxScores = new float[stepCount];
StepFeedback = new string[stepCount];
StepCorrect = new bool[stepCount];
TotalWeightedScore = 0f;
}
/// <summary>
/// 记录一个考核步骤的结果。
/// proportion 为 0~1,表示得分比例(SequenceClick 类型部分正确时用到)。
/// </summary>
public void RecordExamResult(int stepIndex, float maxScore, float proportion, string feedback)
{
if (StepScores == null || stepIndex >= StepScores.Length) return;
StepMaxScores[stepIndex] = maxScore;
float earned = maxScore * Mathf.Clamp01(proportion);
StepScores[stepIndex] = earned;
StepFeedback[stepIndex] = feedback;
StepCorrect[stepIndex] = proportion > 0f;
TotalWeightedScore += earned;
}
/// <summary>按百分比返回考核分数</summary>
public float GetExamPercentage()
{
if (MaxWeightedScore <= 0f) return 0f;
return TotalWeightedScore / MaxWeightedScore * 100f;
}
/// <summary>获取总分(整数)</summary>
public int GetExamScoreRounded()
{
return Mathf.RoundToInt(TotalWeightedScore);
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c57598038a100447ab7f5396d0f0e49c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+172
View File
@@ -0,0 +1,172 @@
using UnityEngine;
using UnityEngine.Events;
/// <summary>
/// 实验流程状态机——整个项目的核心控制器。
/// 负责步骤推进、模式切换、UI 更新、加权考核判分调度。
/// 挂在一个持久化的 GameObject 上。
/// </summary>
public class StepManager : MonoBehaviour
{
[Header("配置")]
public ExperimentConfig Config;
[Header("事件(拖入场景对象或通过代码绑定)")]
public UnityEvent<int> OnStepChanged; // 步骤切换时触发,传新步骤索引
public UnityEvent<string> OnTipChanged; // Tip 文案更新
public UnityEvent<string> OnDialogueChanged; // 角色对话更新
public UnityEvent<StepData> OnExamPopup; // 考核弹窗触发,传当前 StepData
public UnityEvent<string> OnDetailedContent; // 详细操作说明更新(甲方补充)
public UnityEvent OnExperimentComplete; // 实验全部完成
public UnityEvent<string, bool> OnVideoToggle; // 画中画视频开关(文件名, 是否激活)
public UnityEvent<bool> OnModeUIUpdate; // 通知 UI 刷新模式按钮状态
// 私有状态
private StepData[] _activeSteps;
private int _currentStepIndex = -1;
private ExperimentMode _mode;
private FixationMethod _fixation;
public int CurrentStepIndex => _currentStepIndex;
public int TotalSteps => _activeSteps != null ? _activeSteps.Length : 0;
public ExperimentMode Mode => _mode;
public FixationMethod Fixation => _fixation;
/// <summary>用 GameManager 的当前设置初始化实验流程</summary>
public void InitializeFromGameManager()
{
var gm = GameManager.Instance;
if (gm == null) { Debug.LogError("StepManager: GameManager.Instance is null!"); return; }
Initialize(gm.CurrentExperimentType, gm.CurrentMode, gm.CurrentFixationMethod);
}
/// <summary>显式指定参数初始化</summary>
public void Initialize(ExperimentType type, ExperimentMode mode, FixationMethod fixation)
{
_mode = mode;
_fixation = fixation;
Config = Resources.Load<ExperimentConfig>($"Configs/{type}Config");
if (Config == null)
{
Debug.LogError($"StepManager: 找不到 ExperimentConfig for {type},请在 Resources/Configs/ 下创建");
return;
}
_activeSteps = Config.GetStepsFor(mode, fixation);
if (_activeSteps == null || _activeSteps.Length == 0)
{
Debug.LogError($"StepManager: 实验 {type} 模式 {mode} 固定方式 {fixation} 无步骤数据!");
return;
}
_currentStepIndex = -1;
if (mode == ExperimentMode.Exam)
GameManager.Instance.InitExamRecord(_activeSteps.Length);
OnModeUIUpdate?.Invoke(mode == ExperimentMode.Exam);
GoToStep(0);
}
/// <summary>跳转到指定步骤(含边界检查)</summary>
public void GoToStep(int index)
{
if (_activeSteps == null) return;
index = Mathf.Clamp(index, 0, _activeSteps.Length - 1);
if (index == _currentStepIndex) return;
_currentStepIndex = index;
var step = _activeSteps[index];
if (_mode == ExperimentMode.Learn)
{
OnTipChanged?.Invoke(step.TipText);
OnDialogueChanged?.Invoke(step.DialogueText);
OnDetailedContent?.Invoke(step.DetailedContent);
if (step.ShowPipVideo)
OnVideoToggle?.Invoke(step.VideoFileName, true);
else
OnVideoToggle?.Invoke(null, false);
}
else
{
OnTipChanged?.Invoke("");
OnDialogueChanged?.Invoke("");
OnDetailedContent?.Invoke("");
OnVideoToggle?.Invoke(null, false);
if (!string.IsNullOrEmpty(step.ExamQuestionText))
OnExamPopup?.Invoke(step);
}
OnStepChanged?.Invoke(_currentStepIndex);
}
/// <summary>下一步</summary>
public void NextStep()
{
if (_activeSteps == null) return;
if (_currentStepIndex >= _activeSteps.Length - 1)
{
CompleteExperiment();
return;
}
GoToStep(_currentStepIndex + 1);
}
/// <summary>上一步</summary>
public void PreviousStep()
{
if (_activeSteps == null || _currentStepIndex <= 0) return;
GoToStep(_currentStepIndex - 1);
}
/// <summary>获取当前步骤数据(只读)</summary>
public StepData GetCurrentStep()
{
if (_activeSteps == null || _currentStepIndex < 0 || _currentStepIndex >= _activeSteps.Length)
return null;
return _activeSteps[_currentStepIndex];
}
/// <summary>
/// 当前步骤互动完成后的回调。
/// proportion: 得分比例 0~1SequenceClick 按子项命中比例,其余全对=1,全错=0)。
/// </summary>
public void OnStepInteractionComplete(float proportion = 1f, string feedback = "")
{
var step = GetCurrentStep();
if (step == null) return;
if (_mode == ExperimentMode.Exam)
{
string fb = string.IsNullOrEmpty(feedback) ? step.ErrorFeedback : feedback;
GameManager.Instance.RecordExamResult(_currentStepIndex, step.ScoreWeight, proportion, fb);
}
if (_mode == ExperimentMode.Learn)
NextStep();
}
/// <summary>实验完成</summary>
private void CompleteExperiment()
{
Debug.Log("StepManager: 实验流程结束");
OnExperimentComplete?.Invoke();
}
/// <summary>切换模式(学习 ↔ 考核),从头开始</summary>
public void SwitchMode(ExperimentMode newMode)
{
GameManager.Instance.CurrentMode = newMode;
Initialize(GameManager.Instance.CurrentExperimentType, newMode, GameManager.Instance.CurrentFixationMethod);
}
/// <summary>切换固定方式,从头开始</summary>
public void SwitchFixation(FixationMethod newFixation)
{
GameManager.Instance.CurrentFixationMethod = newFixation;
Initialize(GameManager.Instance.CurrentExperimentType, GameManager.Instance.CurrentMode, newFixation);
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a2535da3ddf9046e686f9b0d2ce5f569
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: