415 lines
13 KiB
C#
415 lines
13 KiB
C#
// TutorialFlightController.cs
|
|
// Zweck: Schritt-für-Schritt Flug-Tutorial (Hebel -> Vorwärts -> Kopfneigung -> Fragen -> Abschluss).
|
|
using System.Collections.Generic;
|
|
using TMPro;
|
|
using UnityEngine;
|
|
using UnityEngine.InputSystem;
|
|
|
|
public class TutorialFlightController : MonoBehaviour
|
|
{
|
|
public enum TutorialPhase
|
|
{
|
|
Idle,
|
|
Welcome,
|
|
ExplainLever,
|
|
WaitForForwardFlight,
|
|
ExplainHeadTilt,
|
|
WaitHeadPlay,
|
|
ExplainQuestions,
|
|
InQuestions,
|
|
FinalCongrats
|
|
}
|
|
|
|
[Header("References")]
|
|
[SerializeField] private ShipMovement shipMovement;
|
|
[SerializeField] private ThrottleLever throttle;
|
|
[SerializeField] private GateSpawner gateSpawner;
|
|
[SerializeField] private CanvasGroup textCanvas;
|
|
[SerializeField] private TMP_Text textLabel;
|
|
[SerializeField] private InputActionReference advanceAction;
|
|
|
|
[Header("Lever Highlight (Pulse)")]
|
|
[SerializeField] private Color pulseColor = Color.cyan;
|
|
[SerializeField] private float pulseSpeed = 2.2f;
|
|
[SerializeField] private float pulseMin = 0.15f;
|
|
[SerializeField] private float pulseMax = 1.0f;
|
|
|
|
[Header("Flow settings")]
|
|
[SerializeField] private float requiredTravelDistance = 100f;
|
|
[SerializeField] private float headPlayDuration = 5f;
|
|
|
|
[Header("Tutorial Q&A")]
|
|
[SerializeField] private int tutorialAnswersTarget = 2;
|
|
|
|
// Zustand
|
|
private TutorialPhase phase = TutorialPhase.Idle;
|
|
private CockpitTransitionController transitionController;
|
|
private Vector3 travelStartPos;
|
|
private float headPlayTimer;
|
|
|
|
// Hebel-Visuals
|
|
private Renderer[] leverRenderers;
|
|
private MaterialPropertyBlock[] mpbs;
|
|
private bool leverPulseActive;
|
|
private Coroutine pulseCo;
|
|
private static readonly int _ColorID = Shader.PropertyToID("_Color");
|
|
private static readonly int _EmissionColorID = Shader.PropertyToID("_EmissionColor");
|
|
|
|
// Pager
|
|
private List<string> _currentPages = new List<string>();
|
|
private int _pageIndex = -1;
|
|
|
|
// Tutorial Q Flow
|
|
private int tutorialAnswers = 0;
|
|
private bool tutorialActive = false;
|
|
|
|
private void Awake()
|
|
{
|
|
if (textCanvas)
|
|
{
|
|
textCanvas.alpha = 0f;
|
|
textCanvas.interactable = false;
|
|
textCanvas.blocksRaycasts = false;
|
|
if (!textCanvas.gameObject.activeSelf) textCanvas.gameObject.SetActive(true);
|
|
}
|
|
}
|
|
|
|
private void OnEnable()
|
|
{
|
|
if (advanceAction != null)
|
|
{
|
|
advanceAction.action.performed += OnAdvance;
|
|
advanceAction.action.Enable();
|
|
}
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
if (advanceAction != null)
|
|
{
|
|
advanceAction.action.performed -= OnAdvance;
|
|
advanceAction.action.Disable();
|
|
}
|
|
|
|
if (gateSpawner != null)
|
|
{
|
|
gateSpawner.OnTutorialQuestionAnswered -= OnTutorialQuestionAnswered;
|
|
gateSpawner.OnTutorialSequenceFinished -= OnTutorialSequenceFinished;
|
|
}
|
|
|
|
StopLeverPulse();
|
|
}
|
|
|
|
// Einstieg: Referenzen setzen, Events binden, Hebel-Renderer sammeln
|
|
public void BeginTutorial(CockpitTransitionController transition, ShipMovement ship, GateSpawner spawner)
|
|
{
|
|
transitionController = transition;
|
|
shipMovement = ship;
|
|
gateSpawner = spawner;
|
|
|
|
if (gateSpawner != null)
|
|
{
|
|
gateSpawner.OnTutorialQuestionAnswered -= OnTutorialQuestionAnswered;
|
|
gateSpawner.OnTutorialSequenceFinished -= OnTutorialSequenceFinished;
|
|
gateSpawner.OnTutorialQuestionAnswered += OnTutorialQuestionAnswered;
|
|
gateSpawner.OnTutorialSequenceFinished += OnTutorialSequenceFinished;
|
|
}
|
|
|
|
if (throttle && throttle.handle)
|
|
{
|
|
leverRenderers = throttle.handle.GetComponentsInChildren<Renderer>(true);
|
|
if (leverRenderers != null && leverRenderers.Length > 0)
|
|
{
|
|
mpbs = new MaterialPropertyBlock[leverRenderers.Length];
|
|
for (int i = 0; i < leverRenderers.Length; i++)
|
|
mpbs[i] = new MaterialPropertyBlock();
|
|
}
|
|
}
|
|
|
|
if (shipMovement)
|
|
{
|
|
shipMovement.SetActive(false);
|
|
shipMovement.HeadTiltEnabled = false;
|
|
}
|
|
|
|
if (throttle) throttle.SetGrabEnabled(true);
|
|
|
|
tutorialAnswers = 0;
|
|
tutorialActive = false;
|
|
|
|
ShowPages(
|
|
"Willkommen im <b>Tutorial</b>! Hier lernst du Schritt für Schritt, wie du das Schiff steuerst.",
|
|
"Um das Schiff zu starten, greife nach dem <b>Hebel</b> rechts vor dir.",
|
|
"Sobald dein rechter Controller den Hebel berührt, halte den <b>rechten Trigger</b> gedrückt und schiebe den Hebel nach vorn, um zu beschleunigen.",
|
|
"Um wieder langsamer zu werden, ziehe den Hebel nach hinten. Probier es aus!"
|
|
);
|
|
phase = TutorialPhase.Welcome;
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
switch (phase)
|
|
{
|
|
case TutorialPhase.WaitForForwardFlight:
|
|
UpdateWaitForForwardFlight();
|
|
break;
|
|
case TutorialPhase.WaitHeadPlay:
|
|
UpdateWaitHeadPlay();
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Pager
|
|
private void ShowPages(params string[] pages)
|
|
{
|
|
_currentPages.Clear();
|
|
if (pages != null) _currentPages.AddRange(pages);
|
|
_pageIndex = -1;
|
|
AdvancePage();
|
|
}
|
|
|
|
private bool AdvancePage()
|
|
{
|
|
if (_currentPages.Count == 0) return false;
|
|
|
|
_pageIndex++;
|
|
if (_pageIndex >= _currentPages.Count) return false;
|
|
|
|
if (textLabel) textLabel.text = _currentPages[_pageIndex];
|
|
|
|
if (textCanvas)
|
|
{
|
|
textCanvas.alpha = 1f;
|
|
textCanvas.interactable = true;
|
|
textCanvas.blocksRaycasts = true;
|
|
if (!textCanvas.gameObject.activeSelf) textCanvas.gameObject.SetActive(true);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private void ClearPages() { _currentPages.Clear(); _pageIndex = -1; }
|
|
|
|
private void HideText()
|
|
{
|
|
if (textCanvas)
|
|
{
|
|
textCanvas.alpha = 0f;
|
|
textCanvas.interactable = false;
|
|
textCanvas.blocksRaycasts = false;
|
|
}
|
|
}
|
|
|
|
private void OnAdvance(InputAction.CallbackContext _)
|
|
{
|
|
if (textCanvas == null || textCanvas.alpha <= 0.01f) return;
|
|
|
|
if (AdvancePage()) return;
|
|
|
|
switch (phase)
|
|
{
|
|
case TutorialPhase.Welcome:
|
|
case TutorialPhase.ExplainLever:
|
|
HideText(); ClearPages();
|
|
HighlightLeverAndEnableFlight();
|
|
break;
|
|
|
|
case TutorialPhase.ExplainHeadTilt:
|
|
HideText(); ClearPages();
|
|
if (shipMovement)
|
|
{
|
|
shipMovement.HeadTiltEnabled = true;
|
|
shipMovement.SetActive(true);
|
|
}
|
|
headPlayTimer = 0f;
|
|
phase = TutorialPhase.WaitHeadPlay;
|
|
break;
|
|
|
|
case TutorialPhase.ExplainQuestions:
|
|
HideText(); ClearPages();
|
|
if (gateSpawner) gateSpawner.BeginTutorialQuestions();
|
|
if (shipMovement)
|
|
{
|
|
shipMovement.SetActive(true);
|
|
shipMovement.HeadTiltEnabled = true;
|
|
}
|
|
tutorialActive = true;
|
|
phase = TutorialPhase.InQuestions;
|
|
break;
|
|
|
|
case TutorialPhase.FinalCongrats:
|
|
HideText(); ClearPages();
|
|
if (gateSpawner) gateSpawner.StopTutorialQuestions();
|
|
if (shipMovement)
|
|
{
|
|
shipMovement.SetActive(false);
|
|
shipMovement.HeadTiltEnabled = true;
|
|
shipMovement.ResetToStartPose();
|
|
}
|
|
if (throttle) throttle.ResetToZero();
|
|
ScoreHUD.Instance?.ResetScore();
|
|
transitionController?.EndTutorialReturnToMenu();
|
|
phase = TutorialPhase.Idle;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Schritte
|
|
private void HighlightLeverAndEnableFlight()
|
|
{
|
|
if (shipMovement)
|
|
{
|
|
shipMovement.HeadTiltEnabled = false; // Tilt off
|
|
shipMovement.SetActive(true); // nur Vorwärts
|
|
travelStartPos = shipMovement.MoveRoot.position;
|
|
}
|
|
|
|
if (throttle) throttle.SetGrabEnabled(true);
|
|
|
|
StartLeverPulse();
|
|
phase = TutorialPhase.WaitForForwardFlight;
|
|
}
|
|
|
|
private void UpdateWaitForForwardFlight()
|
|
{
|
|
if (!shipMovement) return;
|
|
|
|
float dist = Vector3.Distance(shipMovement.MoveRoot.position, travelStartPos);
|
|
if (dist >= requiredTravelDistance)
|
|
CompleteForwardFlightStep();
|
|
}
|
|
|
|
private void CompleteForwardFlightStep()
|
|
{
|
|
if (shipMovement) shipMovement.SetActive(false);
|
|
StopLeverPulse();
|
|
|
|
ShowPages(
|
|
"Super!",
|
|
"Jetzt versuche das Schiff nach links und rechts zu steuern",
|
|
"Dafür <b>neigst</b> du deinen Kopf einfach zur linken oder rechten Seite."
|
|
);
|
|
phase = TutorialPhase.ExplainHeadTilt;
|
|
}
|
|
|
|
private void UpdateWaitHeadPlay()
|
|
{
|
|
headPlayTimer += Time.deltaTime;
|
|
if (headPlayTimer >= headPlayDuration) CompleteHeadPlayStep();
|
|
}
|
|
|
|
private void CompleteHeadPlayStep()
|
|
{
|
|
if (shipMovement) shipMovement.SetActive(false);
|
|
ShowPages(
|
|
"Sehr schön. Dann kann ja jetzt nichts mehr <b>schief</b> gehen!",
|
|
"Gleich blende ich dir ein Wort auf der <b>Konsole</b> ein und du musst die korrekte Übersetzung finden.",
|
|
"Vor dir werden die Worte auf der Bahn auftauchen. Fahre einfach <b>über</b> das korrekte Wort <b>rüber</b>.",
|
|
"Für jede korrekte Antwort gibt es <b>Punkte</b>.",
|
|
"Sind die Worte zu weit weg, kannst du sie direkt <b>angucken</b>. Dadurch erscheinen sie bei dir größer auf dem <b>Heads Up Display</b>.",
|
|
"Los geht's!"
|
|
);
|
|
phase = TutorialPhase.ExplainQuestions;
|
|
}
|
|
|
|
// GateSpawner-Callbacks
|
|
private void OnTutorialQuestionAnswered(bool _)
|
|
{
|
|
if (!tutorialActive) return;
|
|
|
|
tutorialAnswers++;
|
|
if (tutorialAnswers >= tutorialAnswersTarget)
|
|
{
|
|
tutorialActive = false;
|
|
if (gateSpawner) gateSpawner.StopTutorialQuestions();
|
|
if (shipMovement) shipMovement.SetActive(false);
|
|
|
|
ShowPages("Na siehst du, ganz einfach oder?");
|
|
phase = TutorialPhase.FinalCongrats;
|
|
}
|
|
}
|
|
|
|
private void OnTutorialSequenceFinished()
|
|
{
|
|
if (!tutorialActive) return;
|
|
|
|
tutorialActive = false;
|
|
if (shipMovement) shipMovement.SetActive(false);
|
|
|
|
ShowPages("Wähle nun einen der Spielmodi. Schnell, bevor noch mehr Wörter aus dieser Welt <b>verschwinden</b>!");
|
|
phase = TutorialPhase.FinalCongrats;
|
|
}
|
|
|
|
// ====== Hebel-Puls ======
|
|
private void StartLeverPulse()
|
|
{
|
|
if (leverPulseActive) return;
|
|
leverPulseActive = true;
|
|
if (pulseCo != null) StopCoroutine(pulseCo);
|
|
pulseCo = StartCoroutine(Co_PulseLever());
|
|
}
|
|
|
|
private void StopLeverPulse()
|
|
{
|
|
leverPulseActive = false;
|
|
if (pulseCo != null)
|
|
{
|
|
StopCoroutine(pulseCo);
|
|
pulseCo = null;
|
|
}
|
|
RestoreLeverVisuals();
|
|
}
|
|
|
|
private System.Collections.IEnumerator Co_PulseLever()
|
|
{
|
|
if (leverRenderers == null || leverRenderers.Length == 0) yield break;
|
|
|
|
// Emission-Keyword vorbereiten
|
|
for (int i = 0; i < leverRenderers.Length; i++)
|
|
{
|
|
var r = leverRenderers[i];
|
|
if (!r) continue;
|
|
r.GetPropertyBlock(mpbs[i]);
|
|
if (r.sharedMaterial && r.sharedMaterial.HasProperty(_EmissionColorID))
|
|
r.sharedMaterial.EnableKeyword("_EMISSION");
|
|
}
|
|
|
|
float t = 0f;
|
|
while (leverPulseActive)
|
|
{
|
|
t += Time.deltaTime * Mathf.Max(0.01f, pulseSpeed) * Mathf.PI * 2f;
|
|
float s = (Mathf.Sin(t) * 0.5f + 0.5f);
|
|
float intensity = Mathf.Lerp(pulseMin, pulseMax, s);
|
|
|
|
Color baseC = pulseColor * intensity;
|
|
|
|
for (int i = 0; i < leverRenderers.Length; i++)
|
|
{
|
|
var r = leverRenderers[i];
|
|
if (!r) continue;
|
|
|
|
var mpb = mpbs[i];
|
|
|
|
if (r.sharedMaterial && r.sharedMaterial.HasProperty(_EmissionColorID))
|
|
mpb.SetColor(_EmissionColorID, baseC);
|
|
else if (r.sharedMaterial && r.sharedMaterial.HasProperty(_ColorID))
|
|
mpb.SetColor(_ColorID, baseC);
|
|
|
|
r.SetPropertyBlock(mpb);
|
|
}
|
|
|
|
yield return null;
|
|
}
|
|
}
|
|
|
|
private void RestoreLeverVisuals()
|
|
{
|
|
if (leverRenderers == null) return;
|
|
for (int i = 0; i < leverRenderers.Length; i++)
|
|
{
|
|
var r = leverRenderers[i];
|
|
if (!r) continue;
|
|
r.SetPropertyBlock(null);
|
|
}
|
|
}
|
|
}
|