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:
@@ -0,0 +1,3 @@
|
||||
using UnityEngine;
|
||||
/// <summary>Legacy — replaced by ExperimentConfig ScriptableObject.</summary>
|
||||
public class ConfigLoader : MonoBehaviour { }
|
||||
@@ -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 串联在一起。
|
||||
/// 考核模式以 proportion(0~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:
|
||||
@@ -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:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c57598038a100447ab7f5396d0f0e49c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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~1(SequenceClick 按子项命中比例,其余全对=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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a2535da3ddf9046e686f9b0d2ce5f569
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user