Files
VFSUnity/Assets/Scripts/Core/StepManager.cs
T

505 lines
18 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using UnityEngine;
using UnityEngine.Events;
/// <summary>
/// 实验流程状态机——整个项目的核心控制器。
/// 负责步骤推进、模式切换、UI 更新、加权考核判分调度。
/// 挂在一个持久化的 GameObject 上。
/// </summary>
public class StepManager : MonoBehaviour
{
[Header("配置(优先级:Inspector直接赋值 > ExperimentFlowCoordinator兜底 > Resources加载)")]
public ExperimentConfig Config;
[Header("事件(拖入场景对象或通过代码绑定)")]
public UnityEvent<int> OnStepChanged;
public UnityEvent<string> OnTipChanged;
public UnityEvent<string> OnDialogueChanged;
public UnityEvent<StepData> OnExamPopup;
public UnityEvent<string> OnDetailedContent;
public UnityEvent OnExperimentComplete;
public UnityEvent<string, bool> OnVideoToggle;
public UnityEvent<bool> OnModeUIUpdate;
// 私有状态
private StepData[] _activeSteps;
private int _currentStepIndex = -1;
private ExperimentMode _mode;
private FixationMethod _fixation;
private bool _initialized = false;
public int CurrentStepIndex => _currentStepIndex;
public int TotalSteps => _activeSteps != null ? _activeSteps.Length : 0;
public ExperimentMode Mode => _mode;
public FixationMethod Fixation => _fixation;
public bool IsInitialized => _initialized;
/// <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)
{
// 如果 Inspector 里没有直接拖 Config,尝试从 ExperimentFlowCoordinator 获取兜底配置
if (Config == null)
{
var coordinator = FindObjectOfType<ExperimentFlowCoordinator>();
if (coordinator != null && coordinator.DefaultConfig != null)
{
Config = coordinator.DefaultConfig;
}
}
// 最后尝试从 Resources 加载
if (Config == null)
{
Config = Resources.Load<ExperimentConfig>($"Configs/{type}Config");
}
if (Config == null)
{
Debug.LogError($"StepManager: 找不到 ExperimentConfig for {type}。" +
"请在 Inspector 中直接拖入 Config,或在 ExperimentFlowCoordinator 上设置 Default Config" +
"或在 Resources/Configs/ 下创建 ScriptableObject。");
return;
}
_mode = mode;
_fixation = fixation;
_activeSteps = Config.GetStepsFor(mode, fixation);
if (_activeSteps == null || _activeSteps.Length == 0)
{
Debug.LogError($"StepManager: 实验 {type} 模式 {mode} 固定方式 {fixation} 无步骤数据!");
return;
}
_currentStepIndex = -1;
_initialized = true;
if (mode == ExperimentMode.Exam)
GameManager.Instance.InitExamRecord(_activeSteps.Length);
OnModeUIUpdate?.Invoke(mode == ExperimentMode.Exam);
GoToStep(0);
}
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);
}
public void NextStep()
{
Debug.LogError("====== StepManager: 接收到 NextStep 指令! ======");
// 首先尝试是否存在可以点击的 3D 物品,如果有,优先执行 3D 交互
if (TryClickActiveHandler())
{
Debug.Log("====== StepManager: 成功模拟 3D 物品点击,不进行常规的 NextStep 流程 ======");
return;
}
Debug.Log("====== StepManager: 未找到 3D 物品,执行常规 NextStep 流程 ======");
if (_activeSteps != null && _currentStepIndex < _activeSteps.Length - 1)
{
GoToStep(_currentStepIndex + 1);
}
else if (_activeSteps != null && _currentStepIndex == _activeSteps.Length - 1)
{
CompleteExperiment();
}
}
public void TryInteractCurrent3DStep()
{
Debug.Log("====== StepManager: 接收到 3D 自动交互指令 ======");
if (TryClickActiveHandler()) return;
Debug.Log("====== StepManager: 当前没有可自动交互的 3D 物品 ======");
}
private bool TryClickActiveHandler()
{
var touches = Object.FindObjectsOfType<TouchPositionHandler>();
System.Array.Sort(touches, (a, b) => string.Compare(a.name, b.name));
foreach (var t in touches)
{
Debug.Log($"[StepManager] 检查 Touch: {t.name}, active={t.gameObject.activeInHierarchy}, CanBeClicked={t.CanBeClicked()}");
if (t.gameObject.activeInHierarchy && t.CanBeClicked())
{
Debug.LogError($"====== StepManager: 模拟点击了 {t.name} ======");
t.PerformClick();
return true;
}
}
var shoses = Object.FindObjectsOfType<ShoseClickHandler>();
foreach (var h in shoses)
{
Debug.Log($"[StepManager] 检查 Shose: {h.name}, active={h.gameObject.activeInHierarchy}, CanBeClicked={h.CanBeClicked()}");
if (h.gameObject.activeInHierarchy && h.CanBeClicked())
{
Debug.LogError($"====== StepManager: 模拟点击了 {h.name} ======");
h.PerformClick();
return true;
}
}
var chanraos = Object.FindObjectsOfType<ChanraoTuibuHandler>();
foreach (var h in chanraos)
{
Debug.Log($"[StepManager] 检查 Chanrao: {h.name}, active={h.gameObject.activeInHierarchy}, CanBeClicked={h.CanBeClicked()}");
if (h.gameObject.activeInHierarchy && h.CanBeClicked())
{
Debug.LogError($"====== StepManager: 模拟点击了 {h.name} ======");
h.PerformClick();
return true;
}
}
var pings = Object.FindObjectsOfType<PingClickHandler>();
System.Array.Sort(pings, (a, b) => string.Compare(b.name, a.name));
foreach (var p in pings)
{
Debug.Log($"[StepManager] 检查 Ping: {p.name}, active={p.gameObject.activeInHierarchy}, CanBeClicked={p.CanBeClicked()}");
if (p.gameObject.activeInHierarchy && p.CanBeClicked())
{
Debug.LogError($"====== StepManager: 模拟点击了 {p.name} ======");
p.PerformClick();
return true;
}
}
return false;
}
private bool TryUndoActiveHandler()
{
if (_pingStepIndex > 0 && _pingStepIndex <= _pingMaxStep)
{
if (_pingObjects == null) InitPingSteps();
PingStepPrevious();
return true;
}
if (_womanStepIndex > 0 && _womanStepIndex <= _womanMaxStep && _pingStepIndex == 0)
{
WomanStepPrevious();
return true;
}
return false;
}
private void PingStepPrevious()
{
if (_pingStepIndex <= 0) return;
int idx = (_pingStepIndex - 1) / 2; // ping index (0-3, mapping to 004-001)
bool wasSecond = (_pingStepIndex - 1) % 2 == 1; // true if was second click
if (wasSecond)
{
// Undo second click: hide chanrao, restore ping to first-click state
if (_chanraoObjects != null && idx < _chanraoObjects.Length && _chanraoObjects[idx] != null)
_chanraoObjects[idx].SetActive(false);
if (_pingObjects != null && idx < _pingObjects.Length && _pingObjects[idx] != null)
{
var h = _pingObjects[idx].GetComponent<PingClickHandler>();
if (h != null) h.UndoSecondClick();
}
}
else
{
// Undo first click: reset current ping to blink state, and hide the next ping it spawned
if (_pingObjects != null && idx < _pingObjects.Length && _pingObjects[idx] != null)
{
var h = _pingObjects[idx].GetComponent<PingClickHandler>();
if (h != null)
{
// 撤销它对下一个目标的影响
if (h.nextPing != null)
{
h.nextPing.SetActive(false);
}
else
{
// 如果是最后一个,撤销它唤醒的所有其他边框
var allPings = Object.FindObjectsOfType<PingClickHandler>();
foreach (var p in allPings)
{
if (p.gameObject != h.gameObject)
{
p.gameObject.SetActive(false);
}
}
}
// 重置自己到初始状态
h.ResetState();
}
}
}
_pingStepIndex--;
Debug.Log($"StepManager: Ping撤销 -> {_pingStepIndex}");
OnStepChanged?.Invoke(_pingStepIndex);
}
private void PingStepNext()
{
if (_pingStepIndex >= _pingMaxStep) return;
int idx = _pingStepIndex / 2;
bool isSecond = _pingStepIndex % 2 == 1;
if (!isSecond)
{
// Advance to first click: ensure ping is visible and in first-click state
if (_pingObjects != null && idx < _pingObjects.Length && _pingObjects[idx] != null)
{
var ping = _pingObjects[idx];
ping.SetActive(true);
var h = ping.GetComponent<PingClickHandler>();
if (h != null)
{
h.UndoSecondClick(); // Reset to first-click state
// Trigger first click via reflection/method
}
}
}
else
{
// Advance to second click: show chanrao
if (_chanraoObjects != null && idx < _chanraoObjects.Length && _chanraoObjects[idx] != null)
_chanraoObjects[idx].SetActive(true);
if (_pingObjects != null && idx < _pingObjects.Length && _pingObjects[idx] != null)
{
var h = _pingObjects[idx].GetComponent<PingClickHandler>();
if (h != null) h.UndoShow();
}
// Show next ping for first click
if (idx + 1 < 4 && _pingObjects != null && _pingObjects[idx+1] != null)
_pingObjects[idx+1].SetActive(true);
}
_pingStepIndex++;
Debug.Log($"StepManager: Ping前进 -> {_pingStepIndex}");
OnStepChanged?.Invoke(_pingStepIndex);
}
public void RecordPingStep(int pingIndex, bool secondClick)
{
_pingStepIndex = pingIndex * 2 + (secondClick ? 2 : 1);
Debug.Log($"StepManager: Ping步进 -> {_pingStepIndex} (ping{pingIndex} second={secondClick})");
OnStepChanged?.Invoke(_pingStepIndex);
}
public void PreviousStep()
{
Debug.LogError("====== StepManager: 接收到 PreviousStep 指令! ======");
// 如果尝试撤销 3D 交互成功,就不再回退 UI
if (TryUndoActiveHandler())
{
Debug.Log("====== StepManager: 成功撤销 3D 交互,不进行常规的 PreviousStep 流程 ======");
return;
}
if (_currentStepIndex > 0)
{
GoToStep(_currentStepIndex - 1);
}
else
{
Debug.LogWarning("====== StepManager: 已经是第一步,无法后退 ======");
}
}
public StepData GetCurrentStep()
{
if (_activeSteps == null || _currentStepIndex < 0 || _currentStepIndex >= _activeSteps.Length)
return null;
return _activeSteps[_currentStepIndex];
}
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();
}
private void CompleteExperiment()
{
Debug.Log("StepManager: 实验流程结束");
OnExperimentComplete?.Invoke();
}
public void SwitchMode(ExperimentMode newMode)
{
GameManager.Instance.CurrentMode = newMode;
Initialize(GameManager.Instance.CurrentExperimentType, newMode, GameManager.Instance.CurrentFixationMethod);
}
public void SwitchFixation(FixationMethod newFixation)
{
GameManager.Instance.CurrentFixationMethod = newFixation;
Initialize(GameManager.Instance.CurrentExperimentType, GameManager.Instance.CurrentMode, newFixation);
}
public void InitWomanSteps()
{
_womanObject = GameObject.Find("woman");
_touchArrayObject = GameObject.Find("touchArray");
// 保存初始位置,用于回退到 step 0
if (_womanObject != null)
{
_womanInitPos = _womanObject.transform.position;
_womanInitRot = _womanObject.transform.rotation;
Debug.Log($"StepManager: woman初始位置已保存 ({_womanInitPos})");
}
var pts = GameObject.Find("womanPointArray");
if (pts != null) { _womanMaxStep = pts.transform.childCount; _womanPositions = new Transform[_womanMaxStep]; for (int i = 0; i < _womanMaxStep; i++) _womanPositions[i] = pts.transform.GetChild(i); }
if (_touchArrayObject != null) { _touchPoints = new GameObject[_touchArrayObject.transform.childCount]; for (int i = 0; i < _touchPoints.Length; i++) _touchPoints[i] = _touchArrayObject.transform.GetChild(i).gameObject; }
_womanStepIndex = 0;
Debug.Log($"StepManager: Woman步进已初始化, 共{_womanMaxStep}个位置");
}
public void InitPingSteps()
{
var mi = GameObject.Find("manInjury");
if (mi == null) return;
_pingObjects = new GameObject[4];
_chanraoObjects = new GameObject[4];
var pings = Object.FindObjectsOfType<PingClickHandler>(true);
foreach (var p in pings)
{
if (p.pingIndex >= 0 && p.pingIndex < 4)
{
_pingObjects[p.pingIndex] = p.gameObject;
if (p.chanraoObject != null)
{
_chanraoObjects[p.pingIndex] = p.chanraoObject;
}
}
}
_pingStepIndex = 0;
Debug.Log("StepManager: Ping步进已初始化");
}
public void RecordWomanStep(int newStepIndex) { _womanStepIndex = Mathf.Clamp(newStepIndex, 0, _womanMaxStep); Debug.Log($"StepManager: Woman步进 -> {_womanStepIndex}"); OnStepChanged?.Invoke(_womanStepIndex); }
private void WomanStepPrevious()
{
if (_womanStepIndex <= 0) return;
_womanStepIndex--;
MoveWomanToStep(_womanStepIndex);
UpdateTouchVisibility(_womanStepIndex);
Debug.Log($"StepManager: Woman撤销 -> {_womanStepIndex}");
OnStepChanged?.Invoke(_womanStepIndex);
}
private void WomanStepNext()
{
if (_womanStepIndex >= _womanMaxStep) return;
_womanStepIndex++;
MoveWomanToStep(_womanStepIndex);
UpdateTouchVisibility(_womanStepIndex);
Debug.Log($"StepManager: Woman前进 -> {_womanStepIndex}");
OnStepChanged?.Invoke(_womanStepIndex);
}
private void MoveWomanToStep(int step)
{
if (_womanObject == null || _womanPositions == null) return;
if (step < 0 || step > _womanPositions.Length) return;
if (step == 0)
{
// 回到初始位置
_womanObject.transform.position = _womanInitPos;
_womanObject.transform.rotation = _womanInitRot;
Debug.Log("StepManager: woman 回到初始位置");
}
else
{
var t = _womanPositions[step - 1];
_womanObject.transform.position = t.position;
_womanObject.transform.rotation = t.rotation;
}
}
private void UpdateTouchVisibility(int step)
{
if (_touchPoints == null) return;
for (int i = 0; i < _touchPoints.Length; i++)
{
bool show = (i == step);
_touchPoints[i].SetActive(show);
if (show)
{
var mr = _touchPoints[i].GetComponent<MeshRenderer>();
if (mr != null) mr.enabled = true;
}
}
}
[Header("=== Woman 步进追踪(不依赖 Config ===")]
[SerializeField] private GameObject _womanObject;
[SerializeField] private GameObject _touchArrayObject;
private int _womanStepIndex = 0;
private int _womanMaxStep = 3;
private Transform[] _womanPositions;
private GameObject[] _touchPoints;
private GameObject[] _pingObjects;
private GameObject[] _chanraoObjects;
private int _pingStepIndex = 0;
private int _pingMaxStep = 8; // 4 pings × 2 clicks each
private Vector3 _womanInitPos;
private Quaternion _womanInitRot;
}