Alles
This commit is contained in:
@@ -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 you’d rather trigger from your textbox script:
|
||||
public void Pulse()
|
||||
{
|
||||
if (anim != null) StopCoroutine(anim);
|
||||
anim = StartCoroutine(PressOnce());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1fdb948a260eb614695e15140ddfc4f5
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: eb1184759173dd54f8e8c6e1245f9baa
|
||||
@@ -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
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a985649e17a992841a0acf24fa83ab74
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 256776785b0a5fe44b69c55d4cf1d2b5
|
||||
@@ -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!");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 81c1c1fa1ea01cc4c9244c79956d6b5e
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c5e9a0ea0088b3f4289482694d1f8580
|
||||
@@ -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
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c2ad52e8a09e80d48a67181c33c5a3b5
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d39aeb541726e44449798b8868e85ffe
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a8e659c46e6266a4f9acdf8fa5e2caff
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 99b3b26f0cc253e40a34a1339e80702d
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 858dc50721aeb9a4a8de7905a787c0f1
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f61ae79ba4636a247a4fedc15e3db775
|
||||
@@ -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
|
||||
@@ -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; we’ll 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 isn’t there
|
||||
menuRoot.SetActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a0e2101bf84d05c448573b4ba1e0df38
|
||||
@@ -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(); }
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e4c624af823ca324686f26f79acf1911
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a5c6dab64dfb9bc48b361b35e568d5f7
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3235685e30c9d9d43a6b8a245cccab9a
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 43997000994945848b300c199d84eda2
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0ed97ac37ce204c4383fbb9b017e6939
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 580fd3636443bfd4486b84c6b28e555d
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6ede7c336a3e689448b6212a4c33089a
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9e73f647735886348802007acf03b2a4
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ac1a28ea0dd0ef34e8d6cd6328df8262
|
||||
Reference in New Issue
Block a user