This commit is contained in:
2025-12-11 13:14:43 +01:00
parent 09c838043f
commit 008efafae0
2079 changed files with 659264 additions and 0 deletions
+93
View File
@@ -0,0 +1,93 @@
using System.Collections;
using UnityEngine;
using UnityEngine.InputSystem;
public class AButtonPress : MonoBehaviour
{
[Header("Target")]
[SerializeField] private RectTransform buttonRect; // your Button image RectTransform
[Header("Input")]
[SerializeField] private InputActionReference advanceAction; // same action you use to advance text
[Header("Active Guard (optional but recommended)")]
[SerializeField] private CanvasGroup textboxCanvas; // the CanvasGroup of your textbox
[SerializeField] private float visibleCutoff = 0.95f; // only animate when alpha >= this
[Header("Motion")]
[SerializeField] private float offsetY = -6f; // “few millimeters” in UI pixels
[SerializeField] private float downTime = 0.06f;
[SerializeField] private float upTime = 0.08f;
Vector2 startPos;
Coroutine anim;
void Awake()
{
if (!buttonRect) buttonRect = GetComponent<RectTransform>();
if (buttonRect) startPos = buttonRect.anchoredPosition;
}
void OnEnable()
{
if (advanceAction != null)
{
advanceAction.action.performed += OnAdvance;
advanceAction.action.Enable();
}
}
void OnDisable()
{
if (advanceAction != null)
{
advanceAction.action.performed -= OnAdvance;
advanceAction.action.Disable();
}
}
void OnAdvance(InputAction.CallbackContext _)
{
if (!buttonRect) return;
// Only while textbox is “on”
if (textboxCanvas && textboxCanvas.alpha < visibleCutoff) return;
if (anim != null) StopCoroutine(anim);
anim = StartCoroutine(PressOnce());
}
IEnumerator PressOnce()
{
Vector2 downPos = startPos + new Vector2(0f, offsetY);
// down
float t = 0f;
while (t < 1f)
{
t += Time.unscaledDeltaTime / downTime;
float k = Mathf.SmoothStep(0f, 1f, Mathf.Clamp01(t));
buttonRect.anchoredPosition = Vector2.LerpUnclamped(startPos, downPos, k);
yield return null;
}
// up
t = 0f;
while (t < 1f)
{
t += Time.unscaledDeltaTime / upTime;
float k = Mathf.SmoothStep(0f, 1f, Mathf.Clamp01(t));
buttonRect.anchoredPosition = Vector2.LerpUnclamped(downPos, startPos, k);
yield return null;
}
anim = null;
}
// If youd rather trigger from your textbox script:
public void Pulse()
{
if (anim != null) StopCoroutine(anim);
anim = StartCoroutine(PressOnce());
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1fdb948a260eb614695e15140ddfc4f5
+126
View File
@@ -0,0 +1,126 @@
// CockpitGateHUD.cs
// Zweck: Kleines HUD im Cockpit, das aktuell fokussierten Gate-Text einblendet (mit Reveal-Animation).
using System.Collections;
using TMPro;
using UnityEngine;
public class CockpitGateHUD : MonoBehaviour
{
[Header("Refs")]
[SerializeField] private TMP_Text label; // Textanzeige
[SerializeField] private CanvasGroup group; // Sichtbarkeit/Interaktion
[SerializeField] private AudioSource sfx; // optional
[SerializeField] private AudioClip revealSfx; // optional: kurzer Blip
[Header("Timings")]
[SerializeField] private float revealTime = 0.45f; // Dauer des Reveals
[Header("Curves")]
[SerializeField] private AnimationCurve scaleCurve = AnimationCurve.EaseInOut(0, 0.7f, 1, 1.02f);
[SerializeField] private AnimationCurve alphaCurve = AnimationCurve.EaseInOut(0, 0f, 1, 1f);
[Header("Debug")]
[SerializeField] private bool debugLogs = false;
private RectTransform _rt;
private Coroutine _revealCo;
private Vector3 _baseScale;
private void Awake()
{
if (!group) group = GetComponent<CanvasGroup>();
_rt = GetComponent<RectTransform>();
if (!_rt) _rt = gameObject.AddComponent<RectTransform>();
_baseScale = _rt.localScale;
// Startzustand: versteckt
if (group)
{
group.alpha = 0f;
group.interactable = false;
group.blocksRaycasts = false;
}
}
// Sofort anzeigen (ohne Reveal)
public void Show(string text)
{
if (debugLogs) Debug.Log($"[CockpitGateHUD] Show: \"{text}\"");
if (label) label.text = text;
StopReveal();
if (group)
{
group.alpha = 1f;
group.interactable = false;
group.blocksRaycasts = false;
}
if (_rt) _rt.localScale = _baseScale;
}
// Mit Reveal-Animation zeigen
public void ShowWithReveal(string text)
{
if (debugLogs) Debug.Log($"[CockpitGateHUD] ShowWithReveal: \"{text}\"");
if (label) label.text = text;
StopReveal();
_revealCo = StartCoroutine(Co_Reveal());
}
// Verstecken (sofort)
public void Hide()
{
if (debugLogs) Debug.Log("[CockpitGateHUD] Hide");
StopReveal();
if (group)
{
group.alpha = 0f;
group.interactable = false;
group.blocksRaycasts = false;
}
if (_rt) _rt.localScale = _baseScale;
}
private void StopReveal()
{
if (_revealCo != null) StopCoroutine(_revealCo);
_revealCo = null;
}
private IEnumerator Co_Reveal()
{
// Vorbereitung
if (group)
{
group.alpha = 0f;
group.interactable = false;
group.blocksRaycasts = false;
}
if (_rt) _rt.localScale = _baseScale * scaleCurve.Evaluate(0f);
// Sound
if (sfx && revealSfx) sfx.PlayOneShot(revealSfx, 0.6f);
float t = 0f;
while (t < revealTime)
{
t += Time.deltaTime;
float n = Mathf.Clamp01(t / revealTime);
if (group) group.alpha = alphaCurve.Evaluate(n);
if (_rt) _rt.localScale = _baseScale * scaleCurve.Evaluate(n);
yield return null;
}
if (group) group.alpha = 1f;
if (_rt) _rt.localScale = _baseScale;
_revealCo = null;
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: eb1184759173dd54f8e8c6e1245f9baa
+120
View File
@@ -0,0 +1,120 @@
// CockpitMenuController.cs
// Zweck: Cockpit-Menü anzeigen/verstecken via Animator; Buttons starten Modi oder beenden Spiel.
using System.Collections;
using UnityEngine;
public class CockpitMenuController : MonoBehaviour
{
[Header("References")]
[SerializeField] private CanvasGroup canvasGroup; // Block für Interaktion/Sichtbarkeit
[SerializeField] private CockpitTransitionController transition; // Modus-Start
[SerializeField] private Animator screenAnim; // Animator des Panels
[Header("Animator parameters (must match Animator)")]
[SerializeField] private string showTrigger = "showScreen";
[SerializeField] private string hideTrigger = "hideScreen";
[SerializeField] private string hideStateName = "Armature|HideAction";
[SerializeField] private string showStateName = "Armature|ShowAction";
[Header("UI timing")]
[SerializeField, Range(0f, 1f)] private float hideAlphaAt = 0.9f; // ab wann während "Hide" die Alpha auf 0 geht
private float showLen;
private float hideLen;
private Coroutine alphaCoroutine;
private void Awake()
{
CacheClipLengths();
HideMenuImmediate(); // Start im versteckten Zustand
}
private void CacheClipLengths()
{
if (!screenAnim || screenAnim.runtimeAnimatorController == null) return;
foreach (var clip in screenAnim.runtimeAnimatorController.animationClips)
{
if (clip.name == showStateName) showLen = clip.length;
else if (clip.name == hideStateName) hideLen = clip.length;
}
if (showLen == 0f || hideLen == 0f)
Debug.LogWarning("[CockpitMenuController] Animationsclips nicht gefunden. State-/Clipnamen prüfen.");
}
public void ShowMenu()
{
if (screenAnim)
{
screenAnim.ResetTrigger(hideTrigger);
screenAnim.SetTrigger(showTrigger);
}
if (alphaCoroutine != null) StopCoroutine(alphaCoroutine);
if (canvasGroup)
{
canvasGroup.alpha = 1f;
canvasGroup.interactable = true;
canvasGroup.blocksRaycasts = true;
}
}
public void HideMenu()
{
if (screenAnim)
{
screenAnim.ResetTrigger(showTrigger);
screenAnim.SetTrigger(hideTrigger);
}
if (canvasGroup)
{
canvasGroup.interactable = false;
canvasGroup.blocksRaycasts = false;
if (alphaCoroutine != null) StopCoroutine(alphaCoroutine);
float delay = hideLen > 0f ? hideLen * hideAlphaAt : 0f;
alphaCoroutine = StartCoroutine(AlphaOffAfter(delay));
}
}
private IEnumerator AlphaOffAfter(float delay)
{
if (delay > 0f) yield return new WaitForSeconds(delay);
if (canvasGroup) canvasGroup.alpha = 0f;
}
private void HideMenuImmediate()
{
if (screenAnim) screenAnim.Play(hideStateName, 0, 1f);
if (canvasGroup)
{
canvasGroup.alpha = 0f;
canvasGroup.interactable = false;
canvasGroup.blocksRaycasts = false;
}
}
// --- Spielmodus-Passthrus ---
public void StartTutorialMode() => transition?.StartTutorialMode();
public void StartModeSequential()
{
GameModeManager.Instance.SelectedMode = GameMode.Default;
transition?.StartSequentialMode();
}
public void StartModeParallel()
{
GameModeManager.Instance.SelectedMode = GameMode.Linear;
transition?.StartParallelMode();
}
public void QuitGame()
{
Debug.Log("Quit requested");
Application.Quit();
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3d9877bbc85c75f4a9f71f2de29766fd
@@ -0,0 +1,290 @@
// CockpitTransitionController.cs
// Zweck: Übergang Hangar <-> Cockpit (Fade, Rig umhängen), Modus/Interfaces schalten, Gameplay/Tutorial starten.
using System.Collections;
using Unity.XR.CoreUtils;
using UnityEngine;
using UnityEngine.XR.Interaction.Toolkit;
using UnityEngine.XR.Interaction.Toolkit.Locomotion;
using UnityEngine.XR.Interaction.Toolkit.Locomotion.Movement;
public class CockpitTransitionController : MonoBehaviour
{
// Rig
[SerializeField] private XROrigin xrOrigin;
[SerializeField] private Transform cameraOffset;
[SerializeField] private ContinuousMoveProvider moveProvider;
[SerializeField] private CharacterController playerCC;
[SerializeField] private GameObject xrDeviceSimulator;
[SerializeField] private XRBodyTransformer bodyTransformer;
[SerializeField] private UnityEngine.XR.Interaction.Toolkit.Interactors.XRBaseInputInteractor rightHandNearFar;
// Schiff
[SerializeField] private Transform shipRoot;
[SerializeField] private ShipMovement shipMovement;
// Welt-Anker
[SerializeField] private Transform hangarSpawnAnchor;
[SerializeField] private Transform cockpitSeatAnchor;
// Fade
[SerializeField] private CanvasGroup fadeCanvas;
[SerializeField] private float fadeDuration = 0.6f;
// UI/Modus
[SerializeField] private GameObject rightHandRay;
[SerializeField] private GameObject hangarUI;
[SerializeField] private GameObject cockpitUI;
[SerializeField] private CockpitTutorialUI tutorialUI;
[SerializeField] private CockpitMenuController menuController;
// Tutorial
[SerializeField] private TutorialFlightController flightTutorial;
// Gates
[SerializeField] private GateSpawner gateSpawner;
private bool inCockpit;
private bool gameplayActive;
private bool isTransitioning;
private void Awake()
{
if (fadeCanvas) fadeCanvas.alpha = 0f;
gameplayActive = false;
// Schiffssysteme anfangs aus
if (shipMovement) shipMovement.SetActive(false);
UpdateMode(true);
}
// -> Cockpit
public void EnterCockpit()
{
if (inCockpit || isTransitioning) return;
StartCoroutine(DoTransition(true));
}
// <- Hangar (optional)
public void ExitCockpit()
{
if (!inCockpit || isTransitioning) return;
StartCoroutine(DoTransition(false));
}
private IEnumerator DoTransition(bool enter)
{
isTransitioning = true;
// Pre
UpdateMode(false);
// Fade + Input blocken
yield return FadeTo(1f, blockInput: true);
// CC/Body kurz deaktivieren (Verkanten vermeiden)
SetCharacterControllerActive(false);
if (enter) AttachRigToShip();
else { DetachRigFromShip(); gameplayActive = false; }
inCockpit = enter;
// CC/Body nur außerhalb Cockpit
SetCharacterControllerActive(!inCockpit);
// Post
UpdateMode(true);
// Fade zurück
yield return FadeTo(0f, blockInput: false);
isTransitioning = false;
}
private void SetCharacterControllerActive(bool active)
{
if (playerCC) playerCC.enabled = active;
if (bodyTransformer) bodyTransformer.enabled = active;
}
private IEnumerator FadeTo(float targetAlpha, bool blockInput)
{
if (!fadeCanvas) yield break;
if (!fadeCanvas.gameObject.activeSelf)
fadeCanvas.gameObject.SetActive(true);
fadeCanvas.blocksRaycasts = blockInput;
fadeCanvas.interactable = blockInput;
float start = fadeCanvas.alpha;
float elapsed = 0f;
while (elapsed < fadeDuration)
{
elapsed += Time.deltaTime;
float t = Mathf.Clamp01(elapsed / fadeDuration);
fadeCanvas.alpha = Mathf.Lerp(start, targetAlpha, t);
yield return null;
}
fadeCanvas.alpha = targetAlpha;
if (Mathf.Approximately(targetAlpha, 0f))
{
fadeCanvas.blocksRaycasts = false;
fadeCanvas.interactable = false;
fadeCanvas.gameObject.SetActive(false);
}
}
// UI/Steuerungsmodi schalten
private void UpdateMode(bool afterTransition)
{
bool enableLocomotion = !inCockpit;
bool enableShipSystems = inCockpit && gameplayActive;
if (moveProvider) moveProvider.enabled = enableLocomotion;
if (shipMovement) shipMovement.SetActive(enableShipSystems);
if (rightHandRay) rightHandRay.SetActive(inCockpit && !gameplayActive);
if (hangarUI) hangarUI.SetActive(!inCockpit);
if (cockpitUI) cockpitUI.SetActive(inCockpit);
if (tutorialUI)
{
if (inCockpit && !gameplayActive && afterTransition) tutorialUI.BeginTutorial();
else tutorialUI.CloseTutorial();
}
if (menuController)
{
if (!inCockpit || gameplayActive) menuController.HideMenu();
}
}
public void OnTutorialFinished()
{
StartCoroutine(ShowMenuNextFrame());
}
private IEnumerator ShowMenuNextFrame()
{
// Einen Frame warten, damit Schließen/Fade fertig ist
yield return null;
menuController?.ShowMenu();
}
// Gameplay nach Moduswahl starten
private void BeginGameplay()
{
UpdateMode(true);
gateSpawner?.BeginGame();
gameplayActive = true;
menuController?.HideMenu();
if (rightHandRay) rightHandRay.SetActive(false);
UpdateMode(true);
}
// Buttons (Passthroughs)
public void StartSequentialMode() { Debug.Log("Mode A selected"); BeginGameplay(); }
public void StartParallelMode() { Debug.Log("Mode B selected"); BeginGameplay(); }
public void StartTutorialMode()
{
Debug.Log("Tutorial practice selected");
// Tutorial als Gameplay behandeln (Bewegung an)
gameplayActive = true;
gateSpawner?.ClearAll();
menuController?.HideMenu();
UpdateMode(true);
if (flightTutorial != null && shipMovement != null && gateSpawner != null)
flightTutorial.BeginTutorial(this, shipMovement, gateSpawner);
else
Debug.LogWarning("[CockpitTransitionController] TutorialFlightController oder Abhängigkeiten fehlen.");
}
// Rig ans Schiff hängen, Sitzposition ausrichten
private void AttachRigToShip()
{
if (!xrOrigin || !shipMovement) return;
var mover = shipMovement.MoveRoot;
if (!mover) return;
// 0) Parent unter den Mover (Pose beibehalten)
xrOrigin.transform.SetParent(mover, worldPositionStays: true);
// 1) Tracking-Origin "Device" (seated)
#if UNITY_XR_CORE_UTILS_2_4_OR_NEWER
xrOrigin.RequestedTrackingOriginMode = UnityEngine.XR.TrackingOriginModeFlags.Device;
xrOrigin.CameraYOffset = 0f;
#else
xrOrigin.RequestedTrackingOriginMode = XROrigin.TrackingOriginMode.Device;
if (xrOrigin.CameraFloorOffsetObject)
{
var off = xrOrigin.CameraFloorOffsetObject.transform;
off.localPosition = Vector3.zero;
off.localRotation = Quaternion.identity;
}
#endif
// 2) Offset zurücksetzen
if (cameraOffset)
{
cameraOffset.localPosition = Vector3.zero;
cameraOffset.localRotation = Quaternion.identity;
cameraOffset.localScale = Vector3.one;
}
// 3) Kamera exakt zur Sitz-Augenposition verschieben
var cam = xrOrigin.Camera;
if (!cam || !cockpitSeatAnchor) return;
const float eyeLift = 0.05f;
Vector3 targetCamPos = cockpitSeatAnchor.position + cockpitSeatAnchor.up * eyeLift;
xrOrigin.MoveCameraToWorldLocation(targetCamPos);
// 4) Yaw an Sitz ausrichten (um die Kameraposition rotieren)
Vector3 camF = cam.transform.forward; camF.y = 0f;
Vector3 seatF = cockpitSeatAnchor.forward; seatF.y = 0f;
if (camF.sqrMagnitude > 1e-4f && seatF.sqrMagnitude > 1e-4f)
{
camF.Normalize(); seatF.Normalize();
float deltaYaw = Vector3.SignedAngle(camF, seatF, Vector3.up);
xrOrigin.transform.RotateAround(cam.transform.position, Vector3.up, deltaYaw);
}
}
public void EndTutorialReturnToMenu()
{
// Tutorial vorbei: Schiff stoppen, Menü anzeigen
gameplayActive = false;
if (shipMovement) shipMovement.SetActive(false);
if (rightHandRay) rightHandRay.SetActive(true);
if (hangarUI) hangarUI.SetActive(false);
if (cockpitUI) cockpitUI.SetActive(true);
menuController?.ShowMenu();
}
// Rig vom Schiff lösen, in Hangar stellen
private void DetachRigFromShip()
{
if (!xrOrigin) return;
xrOrigin.transform.SetParent(null, true);
if (hangarSpawnAnchor)
{
xrOrigin.MoveCameraToWorldLocation(hangarSpawnAnchor.position);
var e = xrOrigin.transform.eulerAngles; e.y = hangarSpawnAnchor.eulerAngles.y;
xrOrigin.transform.eulerAngles = e;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 90822a0f85b1a2a438f38c52a1faf090
+102
View File
@@ -0,0 +1,102 @@
// CockpitTutorialUI.cs
// Zweck: Kurztexte im Cockpit vor Moduswahl, Seitenweise per Input weiterklicken, bei Ende Menü triggern.
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.InputSystem;
public class CockpitTutorialUI : MonoBehaviour
{
[SerializeField] private CanvasGroup canvasGroup; // Sichtbarkeit/Interaktion
[SerializeField] private TMP_Text bodyText; // Textfeld
[SerializeField] private InputActionReference advanceAction; // Eingabe (weiter)
[SerializeField] private float fadeSpeed = 6f; // Fade-Out
[SerializeField] private CockpitTransitionController transition;
[TextArea(2, 4)]
[SerializeField]
private List<string> pages = new()
{
"Willkommen im <b>Cockpit</b>! Falls du noch nicht weißt, wie man das Schiff steuert, schau dir am besten erst das kurze <b>Tutorial</b> an.",
"Nicht, dass du noch einen Unfall baust.",
"Wenn du schon alles kennst, starte direkt mit einem der Spielmodi.",
"Du wählst einen Spielmodus im Menü mit Hilfe des linken Triggers aus.",
"Beeilen wir uns, bevor noch mehr Worte in Vergessenheit geraten!"
};
private int pageIndex = -1;
private bool visible;
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();
}
}
private void Update()
{
if (!canvasGroup) return;
float target = visible ? 1f : 0f;
canvasGroup.alpha = Mathf.MoveTowards(canvasGroup.alpha, target, fadeSpeed * Time.deltaTime);
bool interactable = canvasGroup.alpha > 0.95f;
canvasGroup.interactable = interactable;
canvasGroup.blocksRaycasts = interactable;
}
public void BeginTutorial()
{
visible = true;
if (canvasGroup)
{
canvasGroup.alpha = 1f;
canvasGroup.interactable = true;
canvasGroup.blocksRaycasts = true;
}
pageIndex = -1;
ShowNextPage();
}
public void CloseTutorial()
{
visible = false;
if (canvasGroup)
{
canvasGroup.interactable = false;
canvasGroup.blocksRaycasts = false;
}
pageIndex = -1;
}
private void OnAdvance(InputAction.CallbackContext _)
{
if (!visible) return;
ShowNextPage();
}
private void ShowNextPage()
{
pageIndex++;
if (pageIndex >= pages.Count)
{
CloseTutorial();
if (transition) transition.OnTutorialFinished();
return;
}
if (bodyText) bodyText.text = pages[pageIndex];
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a985649e17a992841a0acf24fa83ab74
+121
View File
@@ -0,0 +1,121 @@
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.InputSystem;
public class EndingCanvas : MonoBehaviour
{
public static EndingCanvas Instance { get; private set; }
[Header("UI")]
[SerializeField] private CanvasGroup canvasGroup;
[SerializeField] private TMP_Text endLabel;
[SerializeField] private Image buttonIcon;
[Header("Input")]
[SerializeField] private InputActionReference returnAction; // e.g. A-Button
[Header("Flow")]
[SerializeField] private CockpitTransitionController transitionController;
[SerializeField] private ShipMovement shipMovement;
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
HideImmediate();
}
private void OnEnable()
{
if (returnAction != null)
{
returnAction.action.performed += OnReturnPressed;
returnAction.action.Enable();
}
}
private void OnDisable()
{
if (returnAction != null)
{
returnAction.action.performed -= OnReturnPressed;
returnAction.action.Disable();
}
}
public void Show(string message)
{
if (endLabel != null)
endLabel.text = message;
if (buttonIcon != null)
buttonIcon.enabled = true;
if (canvasGroup != null)
{
canvasGroup.alpha = 1f;
canvasGroup.interactable = false;
canvasGroup.blocksRaycasts = false;
}
}
public void Hide()
{
if (canvasGroup != null)
{
canvasGroup.alpha = 0f;
canvasGroup.interactable = false;
canvasGroup.blocksRaycasts = false;
}
if (buttonIcon != null)
buttonIcon.enabled = false;
}
private void HideImmediate()
{
if (canvasGroup != null)
{
canvasGroup.alpha = 0f;
canvasGroup.interactable = false;
canvasGroup.blocksRaycasts = false;
}
if (buttonIcon != null)
buttonIcon.enabled = false;
}
private void OnReturnPressed(InputAction.CallbackContext ctx)
{
// Only react if the screen is actually visible
if (canvasGroup == null || canvasGroup.alpha < 0.9f)
return;
// Optional: stop the ship + reset pose (safety)
if (shipMovement != null)
{
shipMovement.SetActive(false);
shipMovement.ResetToStartPose();
}
// Optional: if you want to hard-reset questions right away:
// QuestionManager.Instance?.ResetToFirstQuestion();
// ScoreHUD.Instance?.ResetScore();
// Back to cockpit menu (reusing tutorial method is fine)
if (transitionController != null)
{
transitionController.EndTutorialReturnToMenu();
// or, if you prefer to add a dedicated method:
// transitionController.EndGameReturnToMenu();
}
Hide();
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 256776785b0a5fe44b69c55d4cf1d2b5
+18
View File
@@ -0,0 +1,18 @@
// EnterShipTrigger.cs
// Zweck: Trigger in Hangar; wenn Player eintritt -> Cockpit-Transition starten.
using UnityEngine;
public class EnterShipTrigger : MonoBehaviour
{
[SerializeField] private CockpitTransitionController transition; // Zielcontroller
[SerializeField] private string playerTag = "Player"; // Tag des Spielers
private void OnTriggerEnter(Collider other)
{
// Sicherstellen, dass es der Spieler ist
if (!other.CompareTag(playerTag)) return;
if (transition != null) transition.EnterCockpit();
else Debug.LogWarning("[ShipEntryTrigger] Kein CockpitTransitionController zugewiesen!");
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 81c1c1fa1ea01cc4c9244c79956d6b5e
+29
View File
@@ -0,0 +1,29 @@
// GameMode.cs / GameModeManager.cs
// Zweck: globaler Spielmodus (Default=verstreut, Linear=Reihe) als Singleton über Szenen.
using UnityEngine;
public enum GameMode
{
Default, // verstreut
Linear // Linie quer über die Bahn
}
public class GameModeManager : MonoBehaviour
{
public static GameModeManager Instance { get; private set; }
public GameMode SelectedMode = GameMode.Default;
private void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c5e9a0ea0088b3f4289482694d1f8580
+58
View File
@@ -0,0 +1,58 @@
// GameSettingsManager.cs
// Zweck: Zentrale Schalter für SFX/VFX/Musik/Score, Singleton + Weitergabe an Subsysteme.
using UnityEngine;
public class GameSettingsManager : MonoBehaviour
{
public static GameSettingsManager Instance { get; private set; }
[Header("Defaults")]
[SerializeField] private bool sfxEnabled = true;
[SerializeField] private bool vfxEnabled = true;
[SerializeField] private bool musicEnabled = true;
[SerializeField] private bool scoreEnabled = true;
public bool SfxEnabled => sfxEnabled;
public bool VfxEnabled => vfxEnabled;
public bool MusicEnabled => musicEnabled;
public bool ScoreEnabled => scoreEnabled;
private void Awake()
{
// Singleton + persistieren
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
// Startzustand auf Systeme anwenden
ApplyAll();
}
private void ApplyAll()
{
SetMusicEnabled(musicEnabled);
SetScoreEnabled(scoreEnabled);
// SFX/VFX werden dort berücksichtigt, wo sie genutzt werden.
}
public void SetSfxEnabled(bool value) { sfxEnabled = value; }
public void SetVfxEnabled(bool value) { vfxEnabled = value; }
public void SetMusicEnabled(bool value)
{
musicEnabled = value;
if (MusicManager.Instance != null)
MusicManager.Instance.SetMuted(!musicEnabled);
}
public void SetScoreEnabled(bool value)
{
scoreEnabled = value;
if (ScoreHUD.Instance != null)
ScoreHUD.Instance.SetVisible(scoreEnabled);
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e3efe74d8994d6a4295c4cf30c4befeb
+34
View File
@@ -0,0 +1,34 @@
// GateAnswer.cs
// Zweck: Kollision mit Gate auswerten (Spieler? richtig/falsch?) und an Spawner melden.
using UnityEngine;
public class GateAnswer : MonoBehaviour
{
public int answerIndex; // Index in der Answer-Liste (Info)
public bool isCorrect; // Markierung ob diese Antwort korrekt ist
public GateSpawner spawner; // Rückrufziel
[SerializeField] private string playerTag = "Player";
private void OnTriggerEnter(Collider other)
{
// Root des Kolliders bestimmen (für Rigidbody/Vehicle)
Transform root = other.attachedRigidbody
? other.attachedRigidbody.transform
: other.transform.root;
if (!root.CompareTag(playerTag))
{
Debug.Log($"Gate '{name}' getroffen von '{root.name}' (kein Player, Tag: {root.tag})");
return;
}
// Spieler: Meldung an Spawner
Debug.Log(isCorrect ? "Correct Answer" : "Wrong Answer");
if (spawner != null)
spawner.HandleGateAnswered(isCorrect);
else
Debug.LogWarning($"Gate '{name}' hat keinen GateSpawner gesetzt.");
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c2ad52e8a09e80d48a67181c33c5a3b5
+119
View File
@@ -0,0 +1,119 @@
// GateCenterDetector.cs
// Zweck: Prüft jedes Frame, welches Gate am nächsten zur Bildmitte liegt, und zeigt dessen Text im HUD.
using UnityEngine;
public class GateCenterDetector : MonoBehaviour
{
[Header("XR Camera")]
[SerializeField] private Camera xrCamera;
[Header("References")]
[SerializeField] private GateSpawner gateSpawner;
[SerializeField] private CockpitGateHUD cockpitHUD;
[Header("Selection")]
[Tooltip("Max. Weltdistanz (0 = ignorieren).")]
[SerializeField] private float maxWorldDistance = 0f;
[Tooltip("Max. quadratische Distanz zur Bildschirmmitte im Viewport (0..1).")]
[SerializeField] private float maxCenterDistSqr = 0.05f;
[Header("Debug")]
[SerializeField] private bool debugLogs = false;
private GateText currentGate;
private void Awake()
{
if (!xrCamera)
{
xrCamera = GetComponent<Camera>();
if (!xrCamera && Camera.main != null)
xrCamera = Camera.main;
}
if (debugLogs)
{
if (!xrCamera) Debug.LogWarning("[GateCenterDetector] XR Camera fehlt.");
if (!gateSpawner) Debug.LogWarning("[GateCenterDetector] GateSpawner fehlt.");
if (!cockpitHUD) Debug.LogWarning("[GateCenterDetector] CockpitGateHUD fehlt.");
}
}
private void Update()
{
if (!xrCamera || !gateSpawner || !cockpitHUD)
return;
var gates = gateSpawner.ActiveGates;
if (gates == null || gates.Count == 0)
{
ClearCurrentGate();
return;
}
GateText bestGate = null;
float bestScore = float.MaxValue;
Vector3 camPos = xrCamera.transform.position;
foreach (var gateObj in gates)
{
if (!gateObj) continue;
var gateQuestion = gateObj.GetComponentInChildren<GateText>();
if (!gateQuestion) continue;
Vector3 worldPos = gateObj.transform.position;
// optional: Distanzlimit
if (maxWorldDistance > 0f)
{
float dist = Vector3.Distance(camPos, worldPos);
if (dist > maxWorldDistance) continue;
}
// in Viewport projizieren
Vector3 v = xrCamera.WorldToViewportPoint(worldPos);
if (v.z <= 0f) continue; // hinter Kamera
if (v.x < 0f || v.x > 1f || v.y < 0f || v.y > 1f) continue; // off-screen
// Distanz zur Mitte (0.5/0.5)
float dx = v.x - 0.5f;
float dy = v.y - 0.5f;
float distSqr = dx * dx + dy * dy;
if (distSqr < bestScore)
{
bestScore = distSqr;
bestGate = gateQuestion;
}
}
if (bestGate != null && (maxCenterDistSqr <= 0f || bestScore <= maxCenterDistSqr))
{
if (bestGate != currentGate)
{
currentGate = bestGate;
string text = currentGate.GetQuestionText();
cockpitHUD.ShowWithReveal(text);
if (debugLogs)
Debug.Log($"[GateCenterDetector] Fokus: {currentGate.name} | Text: {text}");
}
}
else
{
ClearCurrentGate();
}
}
private void ClearCurrentGate()
{
if (currentGate != null)
{
if (debugLogs) Debug.Log("[GateCenterDetector] Fokus verloren, HUD aus.");
currentGate = null;
cockpitHUD.Hide();
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f02f8ca1f7d5cf841964f1d6ad4beac1
+541
View File
@@ -0,0 +1,541 @@
// GateSpawner.cs
// Zweck: Spawnt Gates zur aktuellen Frage (Tutorial/JSON), setzt Frage-UI, wertet Ergebnisse aus und geht weiter.
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public class GateSpawner : MonoBehaviour
{
// === Prefabs & Referenzen ===
public GameObject gatePrefab;
public Transform player; // Schiffstransform
public Transform roadCenter; // Bahnmittelpunkt (Fallback: player)
public TMP_Text questionTextDisplay;
public RawImage questionImageDisplay;
public AudioSource questionAudioSource;
[Header("Ship Feedback")]
[SerializeField] private ShipFeedbackController shipFeedback;
[Header("Ship")]
[SerializeField] private ShipMovement shipMovement;
// === SFX ===
public AudioSource sfxSource;
public string correctClipName = "correctSparkles";
public string wrongClipName = "wrongShock";
public float sfxVolume = 1f;
private AudioClip _correctClip;
private AudioClip _wrongClip;
// === Gate Layout ===
public int maxGatesPerQuestion = 5;
public float spacing = 120f;
public float spawnAheadDistance = 1000f;
public float roadHalfWidth = 80f;
// Boden/Höhe
public float fallbackGroundY = 0f;
public LayerMask groundLayer;
// Sicherheits-Trigger wenn alle Gates übersprungen werden
public float skipTriggerOffset = 25f;
public Vector2 skipTriggerSize = new Vector2(80f, 20f);
public float skipTriggerThickness = 5f;
[Header("Road bounds")]
[SerializeField] private float edgePadding = 2f; // Randabstand
private GameObject activeSkipTrigger;
private readonly List<GameObject> activeGates = new();
// === Tutorial (fixe Fragen) ===
[Header("Tutorial mode (fixed questions)")]
[SerializeField] private Question[] tutorialQuestions;
private int _tutorialIndex = 0;
private bool _tutorialActive = false;
private int _tutorialAnswered = 0;
[SerializeField] private int tutorialTargetAnswers = 2;
// Events für TutorialFlightController
public System.Action<bool> OnTutorialQuestionAnswered;
public System.Action OnTutorialSequenceFinished;
private void Awake()
{
// Fragebild initial aus
if (questionImageDisplay != null)
questionImageDisplay.gameObject.SetActive(false);
// SFX laden
_correctClip = Resources.Load<AudioClip>(correctClipName);
_wrongClip = Resources.Load<AudioClip>(wrongClipName);
if (!_correctClip) Debug.LogWarning($"Correct SFX nicht gefunden: {correctClipName}");
if (!_wrongClip) Debug.LogWarning($"Wrong SFX nicht gefunden: {wrongClipName}");
}
// === JSON-Spielstart ===
public void BeginGame()
{
_tutorialActive = false;
_tutorialIndex = 0;
_tutorialAnswered = 0;
ScoreHUD.Instance?.ResetScore();
QuestionManager.Instance?.ResetToFirstQuestion();
EnsureShipMovement();
if (shipMovement) shipMovement.ResetAdaptiveSpeed();
SpawnCurrentQuestionGates();
}
// Alles leeren (Gates/Frageanzeige/Audio)
public void ClearAll()
{
ClearExistingGates();
if (questionTextDisplay) questionTextDisplay.text = "";
if (questionImageDisplay) questionImageDisplay.gameObject.SetActive(false);
if (questionAudioSource) questionAudioSource.Stop();
}
// Lazy-Referenzen beschaffen
private void EnsureShipFeedback()
{
if (shipFeedback != null || player == null) return;
shipFeedback = player.GetComponentInChildren<ShipFeedbackController>();
if (!shipFeedback) Debug.LogWarning("[GateSpawner] ShipFeedbackController nicht gefunden.");
}
private void EnsureShipMovement()
{
if (shipMovement != null || player == null) return;
shipMovement = player.GetComponentInChildren<ShipMovement>();
if (!shipMovement) Debug.LogWarning("[GateSpawner] ShipMovement nicht gefunden.");
}
// === Tutorial-Ablauf ===
public void BeginTutorialQuestions()
{
_tutorialActive = true;
_tutorialIndex = 0;
_tutorialAnswered = 0;
ClearAll();
ScoreHUD.Instance?.ResetScore();
if (tutorialQuestions != null && tutorialQuestions.Length > 0)
SpawnGatesForQuestion(tutorialQuestions[_tutorialIndex]);
else
{
Debug.LogWarning("[GateSpawner] Keine Tutorialfragen zugewiesen.");
_tutorialActive = false;
OnTutorialSequenceFinished?.Invoke();
}
}
public void StopTutorialQuestions()
{
_tutorialActive = false;
_tutorialIndex = 0;
_tutorialAnswered = 0;
ClearAll();
}
private void SpawnNextTutorialQuestion()
{
_tutorialIndex++;
if (tutorialQuestions == null || _tutorialIndex >= tutorialQuestions.Length)
{
ClearAll();
_tutorialActive = false;
OnTutorialSequenceFinished?.Invoke();
}
else
{
SpawnGatesForQuestion(tutorialQuestions[_tutorialIndex]);
}
}
// === JSON-Flow: aktuelle Frage spawnen ===
public void SpawnCurrentQuestionGates()
{
ClearExistingGates();
if (!player)
{
Debug.LogWarning("[GateSpawner] Player-Transform fehlt.");
return;
}
var q = QuestionManager.Instance != null ? QuestionManager.Instance.GetCurrentQuestion() : null;
if (q == null)
{
if (questionTextDisplay) questionTextDisplay.text = "";
if (questionImageDisplay) questionImageDisplay.gameObject.SetActive(false);
return;
}
SpawnGatesForQuestion(q);
}
// === Gemeinsamer Spawner (Tutorial + JSON) ===
private void SpawnGatesForQuestion(Question q)
{
if (q == null) { Debug.LogWarning("[GateSpawner] Frage ist null."); return; }
if (!player) { Debug.LogWarning("[GateSpawner] Player-Transform fehlt."); return; }
// Antworten filtern (brauchen Text ODER Bild)
var raw = q.answers != null ? new List<Answer>(q.answers) : new List<Answer>();
var usable = new List<Answer>();
foreach (var a in raw)
{
if (a == null) continue;
bool hasText = !string.IsNullOrWhiteSpace(a.text);
bool hasImage = !string.IsNullOrWhiteSpace(a.image);
if (hasText || hasImage) usable.Add(a);
}
if (usable.Count == 0)
{
Debug.LogWarning("Frage hat keine verwendbaren Antworten.");
if (questionTextDisplay) questionTextDisplay.text = "";
if (questionImageDisplay) questionImageDisplay.gameObject.SetActive(false);
return;
}
int gatesToSpawn = Mathf.Min(maxGatesPerQuestion, usable.Count);
// Shuffle Kopie für die Platzierung
var shuffled = new List<Answer>(usable);
ShuffleList(shuffled);
// Korrekte Antwort bestimmen (Index clampen)
int clampedCorrect = Mathf.Clamp(q.correctAnswerIndex, 0, usable.Count - 1);
Answer correctAnswer = usable[clampedCorrect];
// Frage-UI (Text)
if (questionTextDisplay != null)
{
if (!string.IsNullOrEmpty(q.questionText))
{
questionTextDisplay.gameObject.SetActive(true);
questionTextDisplay.text = q.questionText;
}
else questionTextDisplay.gameObject.SetActive(false);
}
// Frage-UI (Bild)
if (questionImageDisplay != null)
{
if (!string.IsNullOrEmpty(q.questionImage))
{
string imageName = System.IO.Path.GetFileNameWithoutExtension(q.questionImage);
Texture2D qTex = Resources.Load<Texture2D>(imageName);
if (qTex != null)
{
questionImageDisplay.texture = qTex;
questionImageDisplay.gameObject.SetActive(true);
}
else
{
Debug.LogWarning("Fragebild nicht gefunden: " + imageName);
questionImageDisplay.gameObject.SetActive(false);
}
}
else questionImageDisplay.gameObject.SetActive(false);
}
// Frage-VO (optional)
if (questionAudioSource != null)
{
if (!string.IsNullOrEmpty(q.questionSound))
{
string soundName = System.IO.Path.GetFileNameWithoutExtension(q.questionSound);
AudioClip clip = Resources.Load<AudioClip>(soundName);
if (clip != null)
{
questionAudioSource.clip = clip;
questionAudioSource.PlayDelayed(0.5f);
}
else Debug.LogWarning("Fragesound nicht gefunden: " + soundName);
}
else questionAudioSource.clip = null;
}
// Platzierungsbasis (Forward/Right)
Transform basis = roadCenter ? roadCenter : player;
Vector3 fwd = Vector3.ProjectOnPlane(basis.forward, Vector3.up).normalized;
if (fwd.sqrMagnitude < 1e-4f) fwd = Vector3.forward;
Vector3 right = Vector3.Cross(Vector3.up, fwd).normalized;
float playerAlong = Vector3.Dot(player.position - basis.position, fwd);
Vector3 startLineCenter = basis.position + fwd * (playerAlong + spawnAheadDistance);
// Nutzbare Breite innerhalb der Straße
float leftEdge = -roadHalfWidth + edgePadding;
float rightEdge = roadHalfWidth - edgePadding;
for (int i = 0; i < gatesToSpawn; i++)
{
Vector3 gatePos;
// Modusabhängige Platzierung
if (GameModeManager.Instance != null && GameModeManager.Instance.SelectedMode == GameMode.Linear)
{
if (gatesToSpawn == 1) gatePos = startLineCenter;
else
{
float t = (float)i / (gatesToSpawn - 1);
float xOffset = Mathf.Lerp(leftEdge, rightEdge, t);
gatePos = startLineCenter + right * xOffset;
}
}
else
{
float xOffset = Random.Range(leftEdge, rightEdge);
gatePos = startLineCenter + fwd * (i * spacing) + right * xOffset;
}
gatePos = SnapToGround(gatePos, fallbackGroundY);
Quaternion gateRot = Quaternion.LookRotation(fwd, Vector3.up);
GameObject gate = Instantiate(gatePrefab, gatePos, gateRot);
// Antwort-Inhalte befüllen
Answer a = shuffled[i];
TMP_Text textComp = gate.GetComponentInChildren<TMP_Text>(true);
if (textComp != null)
{
if (!string.IsNullOrEmpty(a.text))
{
textComp.gameObject.SetActive(true);
textComp.text = a.text;
}
else textComp.gameObject.SetActive(false);
}
var gateQuestion = gate.GetComponentInChildren<GateText>();
if (gateQuestion != null) gateQuestion.SetQuestionText(a.text);
RawImage rawImg = gate.GetComponentInChildren<RawImage>(true);
Image uiImg = rawImg ? null : gate.GetComponentInChildren<Image>(true);
if (rawImg != null || uiImg != null)
{
if (!string.IsNullOrEmpty(a.image))
{
string imageName = System.IO.Path.GetFileNameWithoutExtension(a.image);
Texture2D tex = Resources.Load<Texture2D>(imageName);
if (tex != null)
{
if (rawImg)
{
rawImg.texture = tex;
rawImg.gameObject.SetActive(true);
}
else
{
uiImg.sprite = SpriteFromTexture(tex);
uiImg.gameObject.SetActive(true);
}
}
else
{
Debug.LogWarning("Antwortbild nicht gefunden: " + imageName);
if (rawImg) rawImg.gameObject.SetActive(false);
if (uiImg) uiImg.gameObject.SetActive(false);
}
}
else
{
if (rawImg) rawImg.gameObject.SetActive(false);
if (uiImg) uiImg.gameObject.SetActive(false);
}
}
var gateAnswer = gate.GetComponentInChildren<GateAnswer>();
if (gateAnswer != null)
{
gateAnswer.answerIndex = usable.IndexOf(a);
gateAnswer.isCorrect = (a == correctAnswer);
gateAnswer.spawner = this;
}
activeGates.Add(gate);
}
if (activeGates.Count > 0)
PlaceSkipTrigger(basis, fwd, right);
}
// === Callback von GateAnswer ===
public void HandleGateAnswered(bool wasCorrect)
{
// 1) SFX/Feedback
var clip = wasCorrect ? _correctClip : _wrongClip;
if (clip != null)
{
if (sfxSource) sfxSource.PlayOneShot(clip, sfxVolume);
else AudioSource.PlayClipAtPoint(clip, player ? player.position : transform.position, sfxVolume);
}
// 2) Adaptive Geschwindigkeit
EnsureShipMovement();
if (shipMovement) shipMovement.ApplyAnswerResult(wasCorrect);
// 3) Score
if (wasCorrect) ScoreHUD.Instance?.AddPoint(1);
// 4) Alte Gates weg
ClearExistingGates();
// 5) Tutorial?
if (_tutorialActive)
{
OnTutorialQuestionAnswered?.Invoke(wasCorrect);
_tutorialAnswered++;
if (_tutorialAnswered >= tutorialTargetAnswers)
{
_tutorialActive = false;
if (questionTextDisplay) questionTextDisplay.text = "";
if (questionImageDisplay) questionImageDisplay.gameObject.SetActive(false);
OnTutorialSequenceFinished?.Invoke();
return;
}
SpawnNextTutorialQuestion();
return;
}
// 6) JSON-Flow: nächste Frage
var qm = QuestionManager.Instance;
if (qm == null)
{
Debug.LogWarning("[GateSpawner] QuestionManager.Instance ist null.");
return;
}
// Nächste Frage anwählen
qm.AdvanceToNextQuestion();
Question nextQuestion = qm.GetCurrentQuestion();
if (nextQuestion == null)
{
// === HIER: Alle Fragen sind beantwortet ===
// Frage-UI leeren
if (questionTextDisplay) questionTextDisplay.text = "";
if (questionImageDisplay) questionImageDisplay.gameObject.SetActive(false);
if (questionAudioSource) questionAudioSource.Stop();
// Schiff anhalten, damit man nicht ins Nichts rauscht
EnsureShipMovement();
if (shipMovement) shipMovement.SetActive(false);
// Endscreen anzeigen (Text + Button-Hinweis)
if (EndingCanvas.Instance != null)
{
EndingCanvas.Instance.Show(
"Glückwunsch! Du hast alle Fragen geschafft.\n\n" +
"Drücke den angezeigten Button, um zum Cockpit-Menü zurückzukehren."
);
}
else
{
Debug.LogWarning("[GateSpawner] EndOfQuestionsScreen.Instance fehlt kein Endbildschirm angezeigt.");
}
return;
}
// Es gibt noch Fragen -> ganz normal nächste Runde spawnen
SpawnGatesForQuestion(nextQuestion);
}
// === Hilfen ===
private void PlaceSkipTrigger(Transform basis, Vector3 fwd, Vector3 right)
{
// Finde weitestes Gate (entlang fwd)
float maxAlong = float.NegativeInfinity;
Vector3 farthestPos = Vector3.zero;
foreach (var gate in activeGates)
{
if (!gate) continue;
Vector3 toGate = gate.transform.position - basis.position;
float along = Vector3.Dot(toGate, fwd);
if (along > maxAlong)
{
maxAlong = along;
farthestPos = gate.transform.position;
}
}
Vector3 triggerPos = farthestPos + fwd * skipTriggerOffset;
triggerPos = SnapToGround(triggerPos, fallbackGroundY);
if (activeSkipTrigger != null) { Destroy(activeSkipTrigger); activeSkipTrigger = null; }
activeSkipTrigger = new GameObject("Safety Trigger");
activeSkipTrigger.layer = gameObject.layer;
activeSkipTrigger.transform.position = triggerPos;
activeSkipTrigger.transform.rotation = Quaternion.LookRotation(fwd, Vector3.up);
var box = activeSkipTrigger.AddComponent<BoxCollider>();
box.isTrigger = true;
box.size = new Vector3(skipTriggerSize.x, skipTriggerSize.y, skipTriggerThickness);
var rb = activeSkipTrigger.AddComponent<Rigidbody>();
rb.isKinematic = true; rb.useGravity = false;
var logic = activeSkipTrigger.AddComponent<SafetyTrigger>();
logic.spawner = this;
}
private Vector3 SnapToGround(Vector3 pos, float fallbackY)
{
if (groundLayer.value != 0)
{
Vector3 origin = pos + Vector3.up * 200f;
if (Physics.Raycast(origin, Vector3.down, out var hit, 500f, groundLayer, QueryTriggerInteraction.Ignore))
{
pos.y = hit.point.y;
return pos;
}
}
pos.y = fallbackY;
return pos;
}
private void ClearExistingGates()
{
foreach (var g in activeGates) if (g) Destroy(g);
activeGates.Clear();
if (activeSkipTrigger != null)
{
Destroy(activeSkipTrigger);
activeSkipTrigger = null;
}
}
private void ShuffleList<T>(List<T> list)
{
for (int i = 0; i < list.Count; i++)
{
int r = Random.Range(i, list.Count);
(list[i], list[r]) = (list[r], list[i]);
}
}
private static Sprite SpriteFromTexture(Texture2D tex)
{
if (!tex) return null;
return Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), new Vector2(0.5f, 0.5f), 100f);
}
public IReadOnlyList<GameObject> ActiveGates => activeGates;
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d39aeb541726e44449798b8868e85ffe
+32
View File
@@ -0,0 +1,32 @@
// GateText.cs
// Zweck: Liefert Frage/Label-Text für ein Gate (Direktfeld, worldLabel oder Fallback TMP-Child).
using TMPro;
using UnityEngine;
public class GateText : MonoBehaviour
{
[TextArea] public string questionText; // expliziter Text
[Header("Optional floating label above gate")]
public TMP_Text worldLabel; // Label im Welt-Raum
public string GetQuestionText()
{
if (!string.IsNullOrWhiteSpace(questionText))
return questionText;
if (worldLabel != null && !string.IsNullOrWhiteSpace(worldLabel.text))
return worldLabel.text;
var tmp = GetComponentInChildren<TMP_Text>();
if (tmp != null && !string.IsNullOrWhiteSpace(tmp.text))
return tmp.text;
return "";
}
public void SetQuestionText(string text)
{
questionText = text;
if (worldLabel) worldLabel.text = text;
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a8e659c46e6266a4f9acdf8fa5e2caff
+55
View File
@@ -0,0 +1,55 @@
// GroundMarkerPulse.cs
// Zweck: Pulsierender Bodenmarker (Skalierung + optionale Farb/Emission-Pulse).
using UnityEngine;
public class GroundMarkerPulse : MonoBehaviour
{
[Header("Scale Pulse")]
public float minScale = 0.8f;
public float maxScale = 1.2f;
public float pulseSpeed = 2f;
[Header("Color Pulse (optional)")]
public Color baseColor = Color.cyan;
public Color pulseColor = Color.white;
public float colorPulseStrength = 0.5f; // 0=kein Farb-Puls
private Vector3 _initialScale;
private Material _mat;
private Color _originalMatColor;
private void Start()
{
_initialScale = transform.localScale;
var renderer = GetComponent<Renderer>();
if (renderer != null)
{
_mat = renderer.material; // eigene Instanz
_originalMatColor = _mat.color;
_mat.color = baseColor;
}
}
private void Update()
{
// Skalenpuls
float t = (Mathf.Sin(Time.time * pulseSpeed) + 1f) * 0.5f;
float scaleFactor = Mathf.Lerp(minScale, maxScale, t);
transform.localScale = new Vector3(
_initialScale.x * scaleFactor,
_initialScale.y,
_initialScale.z * scaleFactor
);
// Farbpuls (optional)
if (_mat != null && colorPulseStrength > 0f)
{
Color target = Color.Lerp(baseColor, pulseColor, t);
_mat.color = Color.Lerp(baseColor, target, colorPulseStrength);
if (_mat.IsKeywordEnabled("_EMISSION"))
_mat.SetColor("_EmissionColor", target);
}
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 99b3b26f0cc253e40a34a1339e80702d
+129
View File
@@ -0,0 +1,129 @@
// HangarTutorialUI.cs
// Zweck: Einfache Paging-UI im Hangar; per Input weiterklicken; blendet sich weich aus.
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.InputSystem;
public class HangarTutorialUI : MonoBehaviour
{
[Header("UI")]
[SerializeField] private CanvasGroup canvasGroup; // Sichtbarkeit/Interaktion
[SerializeField] private TMP_Text bodyText; // Textfeld
[Header("Controls")]
[SerializeField] private InputActionReference advanceAction; // Eingabe zum Weiterklicken
[Header("Behaviour")]
[SerializeField] private bool startOnAwake = true; // direkt beim Laden starten
[SerializeField] private float fadeSpeed = 6f; // Fade-Out Geschwindigkeit
[TextArea(2, 4)]
[SerializeField]
private List<string> pages = new()
{
"Willkommen zu deiner <b>Mission</b>, Rekrut! Ich bin Solan, deine persönliche <b>KI</b> und unterstütze dich mit allem was du brauchst.",
"Wie du sicherlich schon mitbekommen hast, verschwinden die <b>Englischen Worte</b> auf dieser Welt...",
"Doch wir sind hier um sie zurückzuholen, damit die Englische Sprache nicht ausstirbt!",
"Lass uns keine Zeit verschwenden! Laufe mit dem <b>linken Stick</b> zu deinem <b>Schiff</b>.",
"Sobald du dort bist, drückst du den <b>rechten Trigger</b>, dann beame ich dich rein."
};
private int pageIndex = -1;
private bool visible;
private void Awake()
{
// Startzustand: versteckt
if (canvasGroup)
{
canvasGroup.alpha = 0f;
canvasGroup.interactable = false;
canvasGroup.blocksRaycasts = false;
}
}
private void Start()
{
if (startOnAwake) BeginTutorial();
}
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();
}
}
private void Update()
{
if (!canvasGroup) return;
// Einblenden sofort, Ausblenden per Fade
float target = visible ? 1f : 0f;
canvasGroup.alpha = Mathf.MoveTowards(canvasGroup.alpha, target, fadeSpeed * Time.deltaTime);
bool interactable = canvasGroup.alpha > 0.95f;
canvasGroup.interactable = interactable;
canvasGroup.blocksRaycasts = interactable;
}
/// <summary>Startet das Hangar-Tutorial (erste Seite direkt sichtbar).</summary>
public void BeginTutorial()
{
visible = true;
if (canvasGroup)
{
canvasGroup.alpha = 1f;
canvasGroup.interactable = true;
canvasGroup.blocksRaycasts = true;
}
pageIndex = -1;
ShowNextPage();
}
/// <summary>Schließt die UI (blendet aus).</summary>
public void CloseTutorial()
{
visible = false; // Update() fade-t aus
if (canvasGroup)
{
canvasGroup.interactable = false;
canvasGroup.blocksRaycasts = false;
}
pageIndex = -1;
}
private void OnAdvance(InputAction.CallbackContext _)
{
if (!visible) return;
ShowNextPage();
}
private void ShowNextPage()
{
pageIndex++;
if (pageIndex >= pages.Count)
{
// Ende: einfach verschwinden
CloseTutorial();
return;
}
if (bodyText) bodyText.text = pages[pageIndex];
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 858dc50721aeb9a4a8de7905a787c0f1
+53
View File
@@ -0,0 +1,53 @@
// HoloPanel.cs
// Zweck: UI-Panel mit Scanlines (UV-Scroll + Puls-Alpha) für Holo-Optik.
using UnityEngine;
using UnityEngine.UI;
public class HoloPanel : MonoBehaviour
{
[SerializeField] private RawImage bg; // Hintergrundbild (Scanlines)
[Header("Scanline motion")]
[SerializeField] private Vector2 uvScroll = new Vector2(0f, -0.2f);
[SerializeField] private float uvTiling = 1.5f;
[Header("Pulse")]
[SerializeField] private float pulseSpeed = 1.6f;
[SerializeField, Range(0f, 1f)] private float minAlpha = 0.35f;
[SerializeField, Range(0f, 1f)] private float maxAlpha = 0.65f;
private RectTransform rt;
private Color baseColor;
private void Awake()
{
if (!bg) bg = GetComponent<RawImage>();
rt = GetComponent<RectTransform>();
baseColor = bg ? bg.color : Color.white;
// Kacheln erlauben
if (bg && bg.texture) bg.texture.wrapMode = TextureWrapMode.Repeat;
if (bg) bg.uvRect = new Rect(0, 0, uvTiling, uvTiling);
}
private void Update()
{
if (!bg) return;
// UV scrollen
var r = bg.uvRect;
float period = (uvTiling <= 0f) ? 1f : 1f / uvTiling;
r.x = Mathf.Repeat(r.x + uvScroll.x * Time.deltaTime, period);
r.y = Mathf.Repeat(r.y + uvScroll.y * Time.deltaTime, period);
bg.uvRect = r;
// Alpha-Puls
float t = 0.5f + 0.5f * Mathf.Sin(Time.time * pulseSpeed);
float a = Mathf.Lerp(minAlpha, maxAlpha, t);
var c = baseColor; c.a = a;
bg.color = c;
// Kleines Wabern
if (rt) rt.localEulerAngles = new Vector3(0, 0, Mathf.Sin(Time.time * 0.7f) * 1.2f);
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f61ae79ba4636a247a4fedc15e3db775
+141
View File
@@ -0,0 +1,141 @@
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
public class MainMenuController : MonoBehaviour
{
[Header("Root UI")]
[SerializeField] private CanvasGroup rootCanvas;
[SerializeField] private GameObject mainPanel;
[SerializeField] private GameObject optionsPanel;
[Header("Input")]
[SerializeField] private InputActionReference openMenuAction;
[Header("Restart")]
[SerializeField] private int restartSceneIndex = 0;
[Header("Option Toggles (optional)")]
[SerializeField] private Toggle sfxToggle;
[SerializeField] private Toggle vfxToggle;
[SerializeField] private Toggle musicToggle;
[SerializeField] private Toggle scoreToggle;
private bool isOpen;
private void Awake()
{
// IMPORTANT: Object stays ACTIVE in hierarchy.
// We only hide visually here.
if (rootCanvas != null)
{
rootCanvas.alpha = 0f;
rootCanvas.interactable = false;
rootCanvas.blocksRaycasts = false;
}
if (mainPanel) mainPanel.SetActive(true);
if (optionsPanel) optionsPanel.SetActive(false);
}
private void Start()
{
// keep internal state in sync
if (rootCanvas != null)
isOpen = rootCanvas.alpha >= 0.95f;
var s = GameSettingsManager.Instance;
if (s != null)
{
if (sfxToggle) sfxToggle.isOn = s.SfxEnabled;
if (vfxToggle) vfxToggle.isOn = s.VfxEnabled;
if (musicToggle) musicToggle.isOn = s.MusicEnabled;
if (scoreToggle) scoreToggle.isOn = s.ScoreEnabled;
}
}
private void OnEnable()
{
if (openMenuAction != null)
{
var a = openMenuAction.action;
a.actionMap?.Enable(); // ensure map is on
a.started += OnOpenMenu; // toggle on key down
a.Enable();
}
}
private void OnDisable()
{
if (openMenuAction != null)
{
var a = openMenuAction.action;
a.started -= OnOpenMenu; // match subscription
a.Disable();
}
}
private void OnOpenMenu(InputAction.CallbackContext ctx)
{
if (isOpen) CloseMenu();
else OpenMenu();
}
public void OpenMenu()
{
isOpen = true;
if (rootCanvas != null)
{
rootCanvas.alpha = 1f;
rootCanvas.interactable = true;
rootCanvas.blocksRaycasts = true;
}
if (mainPanel) mainPanel.SetActive(true);
if (optionsPanel) optionsPanel.SetActive(false);
}
public void CloseMenu()
{
isOpen = false;
if (rootCanvas != null)
{
rootCanvas.alpha = 0f;
rootCanvas.interactable = false;
rootCanvas.blocksRaycasts = false;
}
}
public void OnOptionsPressed()
{
if (mainPanel) mainPanel.SetActive(false);
if (optionsPanel) optionsPanel.SetActive(true);
}
public void OnRestartPressed()
{
// if (Time.timeScale == 0f) Time.timeScale = 1f;
SceneManager.LoadScene(restartSceneIndex);
}
public void OnQuitPressed()
{
#if UNITY_EDITOR
UnityEditor.EditorApplication.isPlaying = false;
#else
Application.Quit();
#endif
}
public void OnBackFromOptions()
{
if (mainPanel) mainPanel.SetActive(true);
if (optionsPanel) optionsPanel.SetActive(false);
}
// Toggles…
public void OnSfxToggleChanged(bool v) { GameSettingsManager.Instance?.SetSfxEnabled(v); }
public void OnVfxToggleChanged(bool v) { GameSettingsManager.Instance?.SetVfxEnabled(v); }
public void OnMusicToggleChanged(bool v) { GameSettingsManager.Instance?.SetMusicEnabled(v); }
public void OnScoreToggleChanged(bool v) { GameSettingsManager.Instance?.SetScoreEnabled(v); }
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 46412316fc597454b912c230f7a1b15c
+72
View File
@@ -0,0 +1,72 @@
using UnityEngine;
using UnityEngine.InputSystem;
public class MenuActivating : MonoBehaviour
{
[Header("Wiring")]
[SerializeField] private InputActionReference openMenuAction;
[SerializeField] private GameObject menuRoot; // can start inactive
[SerializeField] private MainMenuController menuController; // on menuRoot (can be null; well fetch)
[Header("Behavior")]
[SerializeField] private bool deactivateOnClose = false;
private void OnEnable()
{
if (openMenuAction == null)
{
Debug.LogError("[MenuActivating] openMenuAction is NULL.");
return;
}
var a = openMenuAction.action;
Debug.Log($"[MenuActivating] Bind -> Action:{a.name} Map:{a.actionMap?.name} Interactions:{a.interactions}");
a.actionMap?.Enable();
a.performed += OnOpenMenu; // <-- use performed (works with Press / Release Only)
a.Enable();
Debug.Log($"[MenuActivating] After enable -> MapEnabled:{a.actionMap?.enabled} ActionEnabled:{a.enabled}");
}
private void OnDisable()
{
if (openMenuAction == null) return;
var a = openMenuAction.action;
a.performed -= OnOpenMenu; // match subscription
a.Disable();
}
private void OnOpenMenu(InputAction.CallbackContext _)
{
// make sure we have the controller (menu may have been inactive at assign time)
if (!menuController && menuRoot) menuController = menuRoot.GetComponent<MainMenuController>();
Debug.Log($"[MenuActivating] Toggle pressed. menuRoot.activeSelf={menuRoot?.activeSelf}");
if (!menuRoot)
{
Debug.LogError("[MenuActivating] menuRoot is NULL. Assign it in the inspector.");
return;
}
if (!menuRoot.activeSelf)
{
// wake it and make it visible
menuRoot.SetActive(true);
if (menuController) menuController.OpenMenu();
else Debug.LogWarning("[MenuActivating] MainMenuController missing; menu may still be alpha=0.");
}
else
{
if (menuController && menuController.enabled)
{
menuController.CloseMenu();
if (deactivateOnClose) menuRoot.SetActive(false);
}
else
{
// fallback: just toggle SetActive if controller isnt there
menuRoot.SetActive(false);
}
}
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a0e2101bf84d05c448573b4ba1e0df38
+90
View File
@@ -0,0 +1,90 @@
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
using UnityEngine.Audio;
public class MusicManager : MonoBehaviour
{
public static MusicManager Instance { get; private set; }
[Header("Clip & Output")]
[Tooltip("The music track to loop.")]
public AudioClip musicClip;
[Tooltip("Optional mixer group output (Master/Music).")]
public AudioMixerGroup outputMixer;
[Header("Playback")]
[Tooltip("Start playback automatically when the game starts.")]
public bool playOnStart = true;
[Tooltip("Loop the music indefinitely.")]
public bool loop = true;
[Header("Controls")]
[Range(0f, 1f)]
[Tooltip("Music volume (0..1).")]
public float volume = 0.6f;
[Tooltip("Mute/unmute music.")]
public bool mute = false;
AudioSource _source;
void Awake()
{
// Singleton guard
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
// AudioSource setup
_source = GetComponent<AudioSource>();
if (_source == null) _source = gameObject.AddComponent<AudioSource>();
_source.playOnAwake = false;
_source.loop = loop;
_source.clip = musicClip;
_source.spatialBlend = 0f; // 2D music
if (outputMixer) _source.outputAudioMixerGroup = outputMixer;
ApplyInspectorSettings();
if (playOnStart && musicClip != null)
_source.Play();
}
void OnValidate()
{
// Keep runtime in sync with inspector tweaks
if (_source != null)
{
ApplyInspectorSettings();
// If you assign a new clip at runtime and playOnStart is true, auto-play it
if (!_source.isPlaying && playOnStart && musicClip != null && Application.isPlaying)
_source.Play();
}
}
void ApplyInspectorSettings()
{
if (_source == null) return;
_source.mute = mute;
_source.volume = Mathf.Clamp01(volume);
_source.loop = loop;
if (_source.clip != musicClip)
{
_source.clip = musicClip;
}
}
// Public helpers if you ever want to drive it from code/UI
public void SetMuted(bool value) { mute = value; ApplyInspectorSettings(); }
public void SetVolume(float value) { volume = Mathf.Clamp01(value); ApplyInspectorSettings(); }
public void Play() { if (musicClip != null) _source.Play(); }
public void Stop() { _source.Stop(); }
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e4c624af823ca324686f26f79acf1911
+13
View File
@@ -0,0 +1,13 @@
using UnityEngine;
public class PlanetRig : MonoBehaviour
{
public Transform xrCamera; // assign your XR camera here
void LateUpdate()
{
if (!xrCamera) return;
// Follow camera position, but keep your own rotation
transform.position = xrCamera.position;
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a5c6dab64dfb9bc48b361b35e568d5f7
+27
View File
@@ -0,0 +1,27 @@
// QuestionData.cs
// Zweck: Datenklassen für Fragen/Antworten (JSON-Format via JsonUtility).
using System;
using UnityEngine;
[Serializable]
public class Answer
{
public string text; // Anzeigen-Text
public string image; // Ressourcenpfad (ohne Endung)
}
[Serializable]
public class Question
{
public string questionText; // Frage-Text
public string questionImage; // Bildname (Ressourcen)
public string questionSound; // Audioclipname (Ressourcen)
public Answer[] answers; // Antwortliste
public int correctAnswerIndex;// Index korrekte Antwort
}
[Serializable]
public class QuestionList
{
public Question[] questions; // Sammlung
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3235685e30c9d9d43a6b8a245cccab9a
+72
View File
@@ -0,0 +1,72 @@
// QuestionManager.cs
// Zweck: Lädt ein gültiges JSON aus Resources (beliebiger Name), verwaltet aktuellen Fragenindex.
using UnityEngine;
public class QuestionManager : MonoBehaviour
{
public QuestionList questionList; // geladene Fragenliste
private int currentQuestionIndex = 0; // aktueller Index
public static QuestionManager Instance { get; private set; }
private void Awake()
{
// Singleton + erhalten über Szenen
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
return;
}
LoadQuestions();
}
private void LoadQuestions()
{
// Alle TextAssets aus Resources durchgehen (keine feste Dateibenennung)
TextAsset[] jsonFiles = Resources.LoadAll<TextAsset>("");
bool foundValidJson = false;
foreach (var file in jsonFiles)
{
// einfache Plausibilitätsprüfung
if (file.text.TrimStart().StartsWith("{"))
{
try
{
questionList = JsonUtility.FromJson<QuestionList>(file.text);
Debug.Log($"JSON geladen: {file.name} mit {questionList.questions.Length} Fragen.");
foundValidJson = true;
return; // erstes gültiges JSON nehmen
}
catch (System.Exception ex)
{
Debug.LogError($"JSON-Parsing fehlgeschlagen ({file.name}): {ex.Message}");
}
}
}
if (!foundValidJson)
Debug.LogError("Kein gültiges JSON im Resources-Ordner gefunden!");
}
public Question GetCurrentQuestion()
{
if (questionList == null || questionList.questions == null) return null;
if (currentQuestionIndex < questionList.questions.Length)
return questionList.questions[currentQuestionIndex];
return null;
}
public int GetCurrentIndex() => currentQuestionIndex;
public void AdvanceToNextQuestion() => currentQuestionIndex++;
public void ResetToFirstQuestion() => currentQuestionIndex = 0;
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 43997000994945848b300c199d84eda2
+164
View File
@@ -0,0 +1,164 @@
// RingGateEffect.cs
// Zweck: Erzeugt „Geister“-Kopien (Meshes), die aufsteigen + verblassen (Holo-Effekt um die Gates).
using System.Collections.Generic;
using UnityEngine;
public class RingGateEffect : MonoBehaviour
{
[Header("Material/Color")]
[Tooltip("Transparent/Unlit empfohlen. Falls leer: benutze Material dieses Objekts.")]
public Material hologramMaterial;
[ColorUsage(true, true)] public Color baseColor = new Color(0.3f, 0.9f, 1f, 0.9f);
[Header("Emission")]
[Min(0f)] public float spawnRate = 0.8f;
[Range(1, 64)] public int maxGhosts = 12;
public bool emitting = true;
public bool burstOnEnable = true;
[Header("Motion & Life")]
[Min(0.1f)] public float lifetime = 2.2f;
public float riseDistance = 0.6f;
public float yawDegPerSec = 35f;
public float startScale = 1f;
public float endScale = 1.12f;
[Header("Curves")]
public AnimationCurve heightOverLife = AnimationCurve.EaseInOut(0, 0, 1, 1);
public AnimationCurve alphaOverLife = new AnimationCurve(new Keyframe(0, 0.9f), new Keyframe(1, 0f));
public AnimationCurve scaleOverLife = new AnimationCurve(new Keyframe(0, 0f), new Keyframe(1, 1f));
private struct Ghost
{
public Transform tf;
public float age;
public Renderer rend;
public MaterialPropertyBlock mpb;
}
private readonly List<Ghost> pool = new();
private readonly List<Ghost> alive = new();
private float spawnAccum;
private Mesh sourceMesh;
private Material sourceMat;
private Transform container;
private static readonly int _BaseColor = Shader.PropertyToID("_BaseColor");
private static readonly int _Color = Shader.PropertyToID("_Color");
private void Awake()
{
var mf = GetComponent<MeshFilter>();
var mr = GetComponent<MeshRenderer>();
if (!mf || !mr || !mf.sharedMesh)
{
Debug.LogError($"[{name}] Benötigt MeshFilter+MeshRenderer mit Mesh.");
enabled = false; return;
}
sourceMesh = mf.sharedMesh;
sourceMat = hologramMaterial ? hologramMaterial : mr.sharedMaterial;
container = new GameObject($"{name}_Ghosts").transform;
container.SetParent(transform, false);
// Pool vorwärmen
for (int i = 0; i < maxGhosts; i++)
{
var go = new GameObject($"Ghost_{i}");
go.transform.SetParent(container, false);
var ghostMF = go.AddComponent<MeshFilter>();
ghostMF.sharedMesh = sourceMesh;
var ghostMR = go.AddComponent<MeshRenderer>();
ghostMR.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
ghostMR.receiveShadows = false;
ghostMR.sharedMaterial = sourceMat;
var g = new Ghost
{
tf = go.transform,
age = lifetime + 1f,
rend = ghostMR,
mpb = new MaterialPropertyBlock()
};
go.SetActive(false);
pool.Add(g);
}
}
private void OnEnable()
{
if (burstOnEnable)
{
for (int i = 0; i < Mathf.Min(4, maxGhosts); i++) Spawn();
}
}
private void Update()
{
float dt = Time.deltaTime;
if (emitting && spawnRate > 0f)
{
spawnAccum += spawnRate * dt;
while (spawnAccum >= 1f)
{
spawnAccum -= 1f;
Spawn();
}
}
// Simulation/Render
for (int i = alive.Count - 1; i >= 0; i--)
{
var g = alive[i];
g.age += dt;
if (g.age >= lifetime)
{
g.tf.gameObject.SetActive(false);
alive.RemoveAt(i);
pool.Add(g);
continue;
}
float t = Mathf.Clamp01(g.age / lifetime);
// Bewegung
float h01 = Mathf.Clamp01(heightOverLife.Evaluate(t));
float y = riseDistance * h01;
float s01 = Mathf.Clamp01(scaleOverLife.Evaluate(t));
float s = Mathf.Lerp(startScale, endScale, s01);
g.tf.localPosition = new Vector3(0f, y, 0f);
g.tf.localRotation = Quaternion.Euler(0f, yawDegPerSec * g.age, 0f);
g.tf.localScale = Vector3.one * s;
// Farbe/Alpha
float a = Mathf.Clamp01(alphaOverLife.Evaluate(t));
var c = baseColor; c.a *= a;
g.mpb.Clear();
g.mpb.SetColor(_BaseColor, c);
g.mpb.SetColor(_Color, c);
g.rend.SetPropertyBlock(g.mpb);
alive[i] = g;
}
}
private void Spawn()
{
if (pool.Count == 0) return;
var g = pool[pool.Count - 1];
pool.RemoveAt(pool.Count - 1);
g.age = 0f;
g.tf.localPosition = Vector3.zero;
g.tf.localRotation = Quaternion.identity;
g.tf.localScale = Vector3.one * startScale;
g.tf.gameObject.SetActive(true);
alive.Add(g);
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0ed97ac37ce204c4383fbb9b017e6939
+14
View File
@@ -0,0 +1,14 @@
// SafetyTrigger.cs
// Zweck: Sicherheits-Netz: wenn Spieler alle Gates überspringt, wird „falsch“ gezählt und weitergemacht.
using UnityEngine;
public class SafetyTrigger : MonoBehaviour
{
public GateSpawner spawner;
private void OnTriggerEnter(Collider other)
{
if (!other.CompareTag("Player")) return;
spawner?.HandleGateAnswered(false);
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 580fd3636443bfd4486b84c6b28e555d
+70
View File
@@ -0,0 +1,70 @@
// ScoreHUD.cs
// Zweck: Einfaches Score-HUD (Singleton): Punkte anzeigen, Sichtbarkeit toggeln.
using TMPro;
using UnityEngine;
public class ScoreHUD : MonoBehaviour
{
public static ScoreHUD Instance { get; private set; }
[Header("UI")]
[SerializeField] private CanvasGroup scoreCanvas; // Canvas/Panel
[SerializeField] private TMP_Text scoreLabel; // Punkte-Text
[Header("Settings")]
[SerializeField] private bool showAtStart = true; // Sichtbar am Start
private int _score;
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
UpdateScoreLabel();
UpdateVisibility();
}
// Punkt hinzufügen
public void AddPoint(int amount = 1)
{
_score += amount;
UpdateScoreLabel();
}
// Score zurücksetzen (neuer Run)
public void ResetScore()
{
_score = 0;
UpdateScoreLabel();
}
// Sichtbarkeit von außen (Optionsmenü)
public void SetVisible(bool visible)
{
showAtStart = visible;
UpdateVisibility();
}
private void UpdateVisibility()
{
if (!scoreCanvas) return;
scoreCanvas.gameObject.SetActive(showAtStart);
scoreCanvas.alpha = showAtStart ? 1f : 0f;
scoreCanvas.interactable = false;
scoreCanvas.blocksRaycasts = false;
}
private void UpdateScoreLabel()
{
if (scoreLabel) scoreLabel.text = _score.ToString();
}
public int CurrentScore => _score;
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6ede7c336a3e689448b6212a4c33089a
+116
View File
@@ -0,0 +1,116 @@
// ShipFeedbackController.cs
// Zweck: Visuelles/Haptisches Feedback bei richtigen/falschen Antworten (Partikel, Shake, Haptik).
using System.Collections;
using UnityEngine;
using UnityEngine.XR;
public class ShipFeedbackController : MonoBehaviour
{
[Header("Shake target")]
[SerializeField] private Transform shakeRoot; // zu schüttelndes Transform
[Header("Particles")]
[SerializeField] private ParticleSystem correctParticles; // Partikel bei richtig
[SerializeField] private ParticleSystem wrongParticles; // Partikel bei falsch
[Header("Haptics (XR Nodes)")]
[SerializeField] private bool useLeftHand = true;
[SerializeField] private bool useRightHand = true;
[SerializeField] private XRNode leftHandNode = XRNode.LeftHand;
[SerializeField] private XRNode rightHandNode = XRNode.RightHand;
[Header("Shake Settings (wrong answer)")]
[SerializeField] private float shakeDuration = 0.25f;
[SerializeField] private float positionIntensity = 0.25f;
[SerializeField] private float rotationIntensity = 6f;
[Header("Haptic Settings")]
[Range(0f, 1f)] [SerializeField] private float correctAmplitude = 0.3f;
[SerializeField] private float correctDuration = 0.12f;
[Range(0f, 1f)] [SerializeField] private float wrongAmplitude = 0.8f;
[SerializeField] private float wrongDuration = 0.25f;
private Coroutine shakeRoutine;
private void Awake()
{
// Fallback: eigenes Transform
if (!shakeRoot) shakeRoot = transform;
}
// Korrektes Tor
public void PlayCorrectFeedback()
{
if (correctParticles) correctParticles.Play();
PlayHaptics(correctAmplitude, correctDuration);
}
// Falsches Tor
public void PlayWrongFeedback()
{
if (wrongParticles) wrongParticles.Play();
if (shakeRoutine != null) StopCoroutine(shakeRoutine);
shakeRoutine = StartCoroutine(Co_Shake());
PlayHaptics(wrongAmplitude, wrongDuration);
}
// Haptik an beide Hände (optional)
private void PlayHaptics(float amplitude, float duration)
{
if (amplitude <= 0f || duration <= 0f) return;
if (useLeftHand) SendHapticToNode(leftHandNode, amplitude, duration);
if (useRightHand) SendHapticToNode(rightHandNode, amplitude, duration);
}
private void SendHapticToNode(XRNode node, float amplitude, float duration)
{
var device = InputDevices.GetDeviceAtXRNode(node);
if (!device.isValid) return;
if (device.TryGetHapticCapabilities(out var caps) && caps.supportsImpulse)
{
device.SendHapticImpulse(0u, amplitude, duration); // Kanal 0 = Standard
}
}
// Einfacher Shake mit Dämpfung
private IEnumerator Co_Shake()
{
if (!shakeRoot) yield break;
Vector3 originalPos = shakeRoot.localPosition;
Quaternion originalRot = shakeRoot.localRotation;
float t = 0f;
while (t < shakeDuration)
{
t += Time.deltaTime;
float n = Mathf.Clamp01(t / shakeDuration);
float damper = 1f - n;
float posMag = positionIntensity * damper;
float rotMag = rotationIntensity * damper;
Vector3 posOffset = new Vector3(
(Random.value - 0.5f) * 2f * posMag,
(Random.value - 0.5f) * 2f * posMag,
(Random.value - 0.5f) * 2f * posMag
);
Vector3 rotOffset = new Vector3(
(Random.value - 0.5f) * 2f * rotMag,
(Random.value - 0.5f) * 2f * rotMag,
(Random.value - 0.5f) * 2f * rotMag
);
shakeRoot.localPosition = originalPos + posOffset;
shakeRoot.localRotation = originalRot * Quaternion.Euler(rotOffset);
yield return null;
}
shakeRoot.localPosition = originalPos;
shakeRoot.localRotation = originalRot;
shakeRoutine = null;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f36755b1667d4664a9ad9fb0e094dfa1
+216
View File
@@ -0,0 +1,216 @@
// ThrottleLever.cs
// Zweck: XR-Hebel entlang definierter Rail bewegen, Output 0..1 als throttle. Stabil gegen XRI/Physik.
using UnityEngine;
using UnityEngine.XR.Interaction.Toolkit;
using UnityEngine.XR.Interaction.Toolkit.Interactables;
using UnityEngine.XR.Interaction.Toolkit.Interactors;
[DefaultExecutionOrder(1000)] // nach den meisten Systemen laufen
[RequireComponent(typeof(XRGrabInteractable))]
public class ThrottleLever : MonoBehaviour
{
[SerializeField] private bool forceStartAtZero = true;
[Header("References")]
public Transform handle; // visueller Hebel
public Transform startPoint; // Rail-Anfang
public Transform endPoint; // Rail-Ende
public Transform space; // Referenzrahmen der Rail
[SerializeField] private Transform shipRoot;
[Header("Output")]
[Range(0f, 1f)] public float throttle = 0f;
[Header("Diagnostics")]
public bool selfTestMode = false;
public float selfTestSpeed = 0.5f;
private XRGrabInteractable grab;
private bool isGrabbed;
private Transform follow; // Attach des Interactors
private float cachedT;
private void Awake()
{
grab = GetComponent<XRGrabInteractable>();
if (!handle || !startPoint || !endPoint)
{
Debug.LogError("[ThrottleLever] handle/start/end zuweisen.");
enabled = false; return;
}
if (!space)
space = startPoint && startPoint.parent ? startPoint.parent : handle.parent;
// sicherstellen, dass space mit dem Schiff mitfährt
if (shipRoot && space && !space.IsChildOf(shipRoot))
{
Vector3 wp = space.position;
Quaternion wq = space.rotation;
space.SetParent(shipRoot, worldPositionStays: true);
space.position = wp;
space.rotation = wq;
Debug.LogWarning("[ThrottleLever] 'space' unter shipRoot angehängt (fährt nun mit).");
}
// Rail-Validierung
var a = space.InverseTransformPoint(startPoint.position);
var b = space.InverseTransformPoint(endPoint.position);
if ((b - a).sqrMagnitude < 1e-6f)
{
Debug.LogError("[ThrottleLever] startPoint und endPoint liegen aufeinander.");
enabled = false; return;
}
// Physik am Handle (statisch, wir bewegen ihn selbst)
var rb = handle.GetComponent<Rigidbody>() ?? handle.gameObject.AddComponent<Rigidbody>();
rb.useGravity = false;
rb.isKinematic = true;
if (!handle.GetComponent<Collider>()) handle.gameObject.AddComponent<BoxCollider>();
// XRI soll unsere Pose NICHT direkt treiben
grab.trackPosition = false;
grab.trackRotation = false;
grab.throwOnDetach = false;
grab.selectEntered.AddListener(OnGrab);
grab.selectExited.AddListener(OnRelease);
Application.onBeforeRender += OnBeforeRenderPose;
}
private void OnDestroy()
{
if (grab)
{
grab.selectEntered.RemoveListener(OnGrab);
grab.selectExited.RemoveListener(OnRelease);
}
Application.onBeforeRender -= OnBeforeRenderPose;
}
private void Start()
{
if (forceStartAtZero)
{
cachedT = 0f; throttle = 0f;
SetHandleByT(cachedT);
}
else
{
ProjectHandleOntoRailFromCurrent();
}
}
private void Update()
{
if (!space) return;
Vector3 a = space.InverseTransformPoint(startPoint.position);
Vector3 b = space.InverseTransformPoint(endPoint.position);
Vector3 ab = b - a;
float abLen2 = ab.sqrMagnitude;
if (abLen2 < 1e-6f) return;
if (selfTestMode)
{
cachedT = 0.5f + 0.5f * Mathf.Sin(Time.time * selfTestSpeed * Mathf.PI * 2f);
}
else if (isGrabbed && follow)
{
Vector3 pLocal = space.InverseTransformPoint(follow.position);
cachedT = Mathf.Clamp01(Vector3.Dot(pLocal - a, ab) / abLen2);
}
else
{
Vector3 hLocal = space.InverseTransformPoint(handle.position);
cachedT = Mathf.Clamp01(Vector3.Dot(hLocal - a, ab) / abLen2);
}
throttle = cachedT;
// Vorläufig schreiben…
SetHandleByT(cachedT);
// …und später noch einmal absichern
}
private void LateUpdate() => SetHandleByT(cachedT);
private void OnBeforeRenderPose() => SetHandleByT(cachedT);
private void SetHandleByT(float t)
{
Vector3 a = space.InverseTransformPoint(startPoint.position);
Vector3 b = space.InverseTransformPoint(endPoint.position);
Vector3 targetLocal = Vector3.Lerp(a, b, Mathf.Clamp01(t));
handle.position = space.TransformPoint(targetLocal);
}
public void SetNormalized(float t)
{
t = Mathf.Clamp01(t);
cachedT = throttle = t;
SetHandleByT(t);
}
public void ResetToZero() => SetNormalized(0f);
private void OnGrab(SelectEnterEventArgs args)
{
isGrabbed = true;
if (args.interactorObject is XRBaseInteractor xrInteractor)
follow = xrInteractor.attachTransform;
else
follow = args.interactorObject.GetAttachTransform(args.interactableObject);
if (!follow)
Debug.LogWarning("[ThrottleLever] attachTransform nicht gefunden.");
if (space && handle.parent != space)
handle.SetParent(space, true);
grab.trackPosition = false;
grab.trackRotation = false;
}
private void OnRelease(SelectExitEventArgs args)
{
isGrabbed = false;
follow = null;
ProjectHandleOntoRailFromCurrent();
if (space && handle.parent != space)
handle.SetParent(space, true);
}
private void ProjectHandleOntoRailFromCurrent()
{
Vector3 a = space.InverseTransformPoint(startPoint.position);
Vector3 b = space.InverseTransformPoint(endPoint.position);
Vector3 ab = b - a; float abLen2 = ab.sqrMagnitude;
if (abLen2 < 1e-6f) return;
Vector3 hLocal = space.InverseTransformPoint(handle.position);
float t = Mathf.Clamp01(Vector3.Dot(hLocal - a, ab) / abLen2);
throttle = cachedT = t;
SetHandleByT(t);
}
public void SetGrabEnabled(bool enabled)
{
if (!grab) grab = GetComponent<XRGrabInteractable>();
if (grab) grab.enabled = enabled;
}
#if UNITY_EDITOR
private void OnDrawGizmosSelected()
{
if (!startPoint || !endPoint) return;
Gizmos.color = Color.yellow;
Gizmos.DrawLine(startPoint.position, endPoint.position);
}
#endif
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9e73f647735886348802007acf03b2a4
+414
View File
@@ -0,0 +1,414 @@
// 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);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5a635c7953591b24da8d8bbba880bcfd
+155
View File
@@ -0,0 +1,155 @@
// ShipMovement.cs
// Zweck: Bewegt das Schiff vorwärts/seitlich, kippt visuell, hält Höhe; adaptive Maxspeed nach Antworten.
using UnityEngine;
public class ShipMovement : MonoBehaviour
{
[Header("Root that actually moves")]
[SerializeField] private Transform moveRoot; // tatsächlicher Mover (Mesh-Root/Cockpit)
// Referenzen
[SerializeField] private Transform headTransform;
[SerializeField] private ThrottleLever throttle;
[SerializeField] private float rideHeight = 0f;
// Vorwärtsbewegung
[SerializeField] private float maxSpeed = 90f;
[SerializeField] private float acceleration = 15f;
[SerializeField] private float deceleration = 20f;
[Header("Adaptive Speed")]
[SerializeField] private bool adaptiveSpeedEnabled = false;
[SerializeField] private float adaptiveMinMaxSpeed = 60f;
[SerializeField] private float adaptiveMaxMaxSpeed = 220f;
[SerializeField] private float speedChangeOnCorrect = 10f;
[SerializeField] private float speedChangeOnWrong = 10f;
private float baseMaxSpeed; // ursprünglicher MaxSpeed (für Reset)
// Strafen/Kippung
[SerializeField] private float strafeSpeed = 60f;
[SerializeField] private float tiltAmount = 20f;
[SerializeField] private float tiltSpeed = 5f;
[SerializeField] private float headRollForFullInput = 30f;
[SerializeField] private float headRollDeadzone = 0.05f;
[SerializeField] private float tiltMinSpeed = 2f;
[SerializeField] private float tiltFullSpeed = 20f;
private float currentSpeed;
private bool isActive;
[SerializeField] private bool lockToRideHeight = true;
[SerializeField] private bool captureHeightOnActivate = true;
// Tutorial
[Header("Tutorial Controls")]
[SerializeField] private bool headTiltEnabled = true;
private Vector3 initialPosition;
private Quaternion initialRotation;
public bool HeadTiltEnabled { get => headTiltEnabled; set => headTiltEnabled = value; }
public float CurrentSpeed => currentSpeed;
public Transform MoveRoot => moveRoot ? moveRoot : transform;
private float capturedY;
public void SetHeadTransform(Transform t) => headTransform = t;
// Aktiviert/Deaktiviert das System (setzt ggf. Höhe)
public void SetActive(bool value)
{
isActive = value;
if (!value) currentSpeed = 0f;
else if (captureHeightOnActivate) capturedY = MoveRoot.position.y;
}
private void Awake()
{
if (!moveRoot) moveRoot = transform;
// Startpose merken (für Tutorial-Reset)
initialPosition = MoveRoot.position;
initialRotation = MoveRoot.rotation;
baseMaxSpeed = maxSpeed;
}
private void Update()
{
if (!isActive || headTransform == null) return;
// Throttle lesen (mit kleiner Deadzone)
float thr = 0f;
if (throttle != null)
{
const float throttleDeadzone = 0.02f;
thr = Mathf.Clamp01(throttle.throttle);
if (thr < throttleDeadzone) thr = 0f;
}
// Beschleunigen/Abbremsen
if (thr > 0f)
currentSpeed += thr * acceleration * Time.deltaTime;
else
currentSpeed = Mathf.MoveTowards(currentSpeed, 0f, deceleration * Time.deltaTime);
// Grenzen
currentSpeed = Mathf.Clamp(currentSpeed, 0f, maxSpeed);
// Kopf-Roll -> seitlicher Input
float headRoll = headTransform.localEulerAngles.z;
if (headRoll > 180f) headRoll -= 360f;
float horizontalInput = -Mathf.Clamp(headRoll / Mathf.Max(0.0001f, headRollForFullInput), -1f, 1f);
if (Mathf.Abs(horizontalInput) < headRollDeadzone) horizontalInput = 0f;
float speedFactor = Mathf.InverseLerp(tiltMinSpeed, tiltFullSpeed, Mathf.Max(0f, currentSpeed));
horizontalInput *= speedFactor;
if (!headTiltEnabled) horizontalInput = 0f;
// Bewegung in Welt (vorwärts + strafe)
Vector3 forwardFlat = MoveRoot.forward; forwardFlat.y = 0f;
if (forwardFlat.sqrMagnitude < 1e-6f) forwardFlat = Vector3.forward; else forwardFlat.Normalize();
Vector3 rightFlat = Vector3.Cross(Vector3.up, forwardFlat);
Vector3 move = forwardFlat * currentSpeed + rightFlat * (horizontalInput * strafeSpeed);
MoveRoot.position += move * Time.deltaTime;
// Visuelle Roll-Kippung
Vector3 e = MoveRoot.eulerAngles;
float targetRoll = -horizontalInput * tiltAmount;
float newZ = Mathf.LerpAngle(e.z, targetRoll, tiltSpeed * Time.deltaTime);
MoveRoot.rotation = Quaternion.Euler(0f, e.y, newZ);
// Höhe sperren (Ride Height)
if (lockToRideHeight)
{
var pos = MoveRoot.position;
pos.y = captureHeightOnActivate ? capturedY : rideHeight;
MoveRoot.position = pos;
}
}
// Adaptive MaxSpeed bei Antwort
public void ApplyAnswerResult(bool wasCorrect)
{
if (!adaptiveSpeedEnabled) return;
float delta = wasCorrect ? speedChangeOnCorrect : -speedChangeOnWrong;
maxSpeed = Mathf.Clamp(maxSpeed + delta, adaptiveMinMaxSpeed, adaptiveMaxMaxSpeed);
if (currentSpeed > maxSpeed) currentSpeed = maxSpeed;
}
public void ResetAdaptiveSpeed()
{
maxSpeed = Mathf.Clamp(baseMaxSpeed, adaptiveMinMaxSpeed, adaptiveMaxMaxSpeed);
if (currentSpeed > maxSpeed) currentSpeed = maxSpeed;
}
// Tutorial: Pose zurücksetzen
public void ResetToStartPose()
{
MoveRoot.position = initialPosition;
MoveRoot.rotation = initialRotation;
currentSpeed = 0f;
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ac1a28ea0dd0ef34e8d6cd6328df8262