A psychological horror corridor experience.
Astral Debt is a short, atmospheric psychological horror game developed for the **GMTK Game Jam 2026**. You find yourself trapped in a surreal, shifting corridor where reality bends between dimensions. As you push forward, the environment changes, testing your sanity and pushing you toward an inescapable truth. FEATURES Surreal Atmosphere: A claustrophobic hallway where dimensions shift around you. Minimalist UI: Clean, immersive experience with visual environmental cues instead of clutter. Psychological Tension: A horror mechanics that keeps you on edge. CONTROLS [WASD] : Move [Ctrl] : Crouch [Space] : Jump [Right Click] : Zoom [Q] : Switch Dimension [F] : Flashlight [E] : Interact (Can Use On Some Walls For Clear) ---------------------------------------- "Shift to the astral realm to seek the truth; those within will show you the way." CREDITS Developed By Kadir Sayir
First Version
using UnityEngine;
using System.Collections;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using UnityEngine.UI;
using TMPro;
public class DimensionManager : MonoBehaviour
{
public static DimensionManager Instance;
public DimensionAudioManager audioManager;
[Header("Parallel Room Parts (Must match in order!)")]
public GameObject[] realParts;
public GameObject[] astralParts;
[Header("Inventory / Keys")]
public int playerKeys = 0;
[Header("Dimension Shift Movement")]
[SerializeField] private float forwardStepAmount = 1.5f;
[HideInInspector] public bool lockSanityAtZero = false;
[Header("Transition Settings")]
public KeyCode switchKey = KeyCode.Q;
public float whitenDuration = 0.2f;
public float transitionCooldown = 1.0f;
private float lastTransitionTime = -999f;
private bool isTransitioning = false;
[Header("Corridor Auto-Shift Settings")]
private Coroutine corridorShiftRoutine = null;
[Header("UI Warnings")]
public TextMeshProUGUI warningText;
private Coroutine hideWarningRoutine;
[Header("Post-Processing Volumes")]
public Volume globalVolume;
private Bloom bloom;
private Vignette vignette;
private ColorAdjustments colorAdjustments;
[Header("References")]
public Transform playerTransform;
public Transform playerCameraTransform;
public GameObject realBodyPrefab;
private GameObject spawnedRealBody;
[Header("Sanity Settings")]
public float maxSanity = 100f;
public float currentSanity;
public float sanityDrainRate = 5f;
public float sanityRefillRate = 30f;
[HideInInspector] public bool isQInputLocked = false;
[Header("Color Thresholds")]
public Color highSanityColor = Color.cyan;
public Color mediumSanityColor = Color.yellow;
public Color lowSanityColor = Color.red;
[Header("UI References")]
public Slider sanitySlider;
public TextMeshProUGUI sanityText;
[Header("Dimension State")]
public bool isAstral = false;
public float interactionDistance = 3f;
private RendererData[] realData;
private RendererData[] astralData;
private struct RendererData
{
public Renderer renderer;
public MaterialPropertyBlock propBlock;
}
private void Awake()
{
if (Instance == null) Instance = this;
else Destroy(gameObject);
}
private void Start()
{
currentSanity = maxSanity;
if (globalVolume != null && globalVolume.profile)
{
globalVolume.profile.TryGet(out bloom);
globalVolume.profile.TryGet(out vignette);
globalVolume.profile.TryGet(out colorAdjustments);
}
CacheRenderers();
SetAllPartsState(false);
if (sanitySlider != null)
{
sanitySlider.maxValue = maxSanity;
sanitySlider.value = currentSanity;
}
}
private void Update()
{
UpdateSanityUI();
HandleSwitchInput();
UpdateSanityDrainOrRefill();
if (sanitySlider != null)
sanitySlider.value = currentSanity;
}
public bool IsAstral() => isAstral;
public void ModifySanity(float amount)
{
currentSanity = Mathf.Clamp(currentSanity + amount, 0, maxSanity);
}
public void ToggleDimension()
{
if (isTransitioning) return;
if (isAstral)
{
audioManager.SwitchToNormalDimension();
StartCoroutine(SmoothReturnToBodyRoutine());
}
else
{
audioManager.SwitchToAstralDimension();
StartCoroutine(CombinedTransitionRoutine());
}
}
public void UpdateCorridorShifting(float intensity)
{
float targetInterval = Mathf.Lerp(3.0f, 0.2f, intensity);
if (corridorShiftRoutine == null)
corridorShiftRoutine = StartCoroutine(CorridorShiftLoop(targetInterval));
}
public void StopAutomaticShifting()
{
if (corridorShiftRoutine != null)
{
StopCoroutine(corridorShiftRoutine);
corridorShiftRoutine = null;
}
ResetToNormalParts();
}
public void ShowTimedWarning(string message, float duration)
{
if (warningText == null) return;
if (hideWarningRoutine != null)
{
StopCoroutine(hideWarningRoutine);
hideWarningRoutine = null;
}
warningText.gameObject.SetActive(true);
warningText.text = message;
hideWarningRoutine = StartCoroutine(HideWarningAfterSeconds(duration));
}
private void UpdateSanityUI()
{
if (sanityText == null) return;
sanityText.text = Mathf.RoundToInt(currentSanity).ToString();
float sanityPercentage = currentSanity / maxSanity;
if (sanityPercentage > 0.6f) sanityText.color = highSanityColor;
else if (sanityPercentage > 0.3f) sanityText.color = mediumSanityColor;
else sanityText.color = lowSanityColor;
}
private void HandleSwitchInput()
{
if (!Input.GetKeyDown(switchKey)) return;
if (isQInputLocked)
{
ShowTimedWarning("Your mind is paralyzed... You cannot escape.", 1.5f);
return;
}
float checkDistance = forwardStepAmount + 0.5f;
Vector3 rayOrigin = playerTransform.position + Vector3.up * 1f;
forwardStepAmount = Physics.Raycast(rayOrigin, playerTransform.forward, out _, checkDistance) ? 0f : 1.5f;
if (!isAstral && currentSanity < 40f)
{
ShowTimedWarning("Low Sanity! Requires at least 40 sanity.", 2f);
return;
}
if (Time.time < lastTransitionTime + transitionCooldown)
{
ShowTimedWarning("Too fast! Please wait.", 1.5f);
return;
}
lastTransitionTime = Time.time;
ToggleDimension();
}
private void UpdateSanityDrainOrRefill()
{
if (isAstral)
{
currentSanity -= sanityDrainRate * Time.deltaTime;
if (currentSanity <= 0)
{
currentSanity = 0;
ForcedReturnToBody();
}
bool shouldShowReturnWarning = currentSanity <= 40f && warningText != null &&
hideWarningRoutine == null &&
(string.IsNullOrEmpty(warningText.text) || warningText.text == "Return to Body!");
if (shouldShowReturnWarning)
{
warningText.gameObject.SetActive(true);
warningText.text = "Return to Body!";
}
}
else
{
if (lockSanityAtZero)
{
currentSanity = 0f;
}
else if (corridorShiftRoutine == null && currentSanity < maxSanity)
{
currentSanity = Mathf.Clamp(currentSanity + sanityRefillRate * Time.deltaTime, 0, maxSanity);
}
bool shouldHideReturnWarning = warningText != null && warningText.text == "Return to Body!" &&
(currentSanity > 40f || !isAstral);
if (shouldHideReturnWarning)
{
if (hideWarningRoutine != null)
{
StopCoroutine(hideWarningRoutine);
hideWarningRoutine = null;
}
warningText.text = "";
warningText.gameObject.SetActive(false);
}
}
}
private IEnumerator CorridorShiftLoop(float interval)
{
bool toggleState = false;
while (true)
{
yield return new WaitForSeconds(interval);
toggleState = !toggleState;
int count = Mathf.Min(realParts.Length, astralParts.Length);
for (int i = 0; i < count; i++)
{
SetRealPartActive(i, !toggleState);
SetAstralPartActive(i, toggleState);
}
float intensity = 1f - (currentSanity / maxSanity);
interval = Mathf.Lerp(3.0f, 0.2f, intensity);
}
}
private void ResetToNormalParts()
{
int count = Mathf.Min(realParts.Length, astralParts.Length);
for (int i = 0; i < count; i++)
{
SetRealPartActive(i, true);
SetAstralPartActive(i, false);
}
}
private void SetAllPartsState(bool astralState)
{
isAstral = astralState;
int count = Mathf.Min(realParts.Length, astralParts.Length);
for (int i = 0; i < count; i++)
{
SetRealPartActive(i, !astralState);
SetAstralPartActive(i, astralState);
}
ResetColors();
}
private void SetRealPartActive(int i, bool showReal)
{
if (!realParts[i]) return;
if (!showReal)
{
realParts[i].SetActive(false);
return;
}
InteractableWall wall = realParts[i].GetComponent<InteractableWall>();
if (wall != null && wall.isBroken)
{
realParts[i].SetActive(false);
if (wall.underlyingWall != null) wall.underlyingWall.SetActive(false);
}
else
{
realParts[i].SetActive(true);
}
}
private void SetAstralPartActive(int i, bool showAstral)
{
if (!astralParts[i]) return;
if (!showAstral)
{
astralParts[i].SetActive(false);
return;
}
bool isBroken = false;
if (realParts[i] != null)
{
InteractableWall wall = realParts[i].GetComponent<InteractableWall>();
if (wall != null && wall.isBroken)
{
isBroken = true;
if (wall.underlyingWall != null) wall.underlyingWall.SetActive(false);
}
}
astralParts[i].SetActive(!isBroken);
}
private IEnumerator HideWarningAfterSeconds(float delay)
{
yield return new WaitForSeconds(delay);
warningText.text = "";
warningText.gameObject.SetActive(false);
hideWarningRoutine = null;
}
private IEnumerator SmoothReturnToBodyRoutine()
{
isTransitioning = true;
float duration = 0.4f;
float elapsed = 0f;
Vector3 startPos = playerTransform.position;
Quaternion startRot = playerTransform.rotation;
Vector3 targetPos = spawnedRealBody != null ? spawnedRealBody.transform.position + Vector3.up * 1.6f : startPos;
Quaternion targetRot = spawnedRealBody != null ? spawnedRealBody.transform.rotation : startRot;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
float t = Mathf.SmoothStep(0f, 1f, elapsed / duration);
playerTransform.position = Vector3.Lerp(startPos, targetPos, t);
playerTransform.rotation = Quaternion.Slerp(startRot, targetRot, t);
yield return null;
}
if (spawnedRealBody != null)
Destroy(spawnedRealBody);
isAstral = false;
ResetToNormalParts();
if (bloom != null) bloom.intensity.value = 2f;
if (vignette != null) vignette.intensity.value = 0.2f;
if (colorAdjustments != null) colorAdjustments.colorFilter.value = Color.white;
ResetColors();
isTransitioning = false;
}
private void ForcedReturnToBody()
{
if (!isTransitioning)
ToggleDimension();
}
private void CacheRenderers()
{
realData = new RendererData[realParts.Length];
for (int i = 0; i < realParts.Length; i++)
{
if (realParts[i])
{
realData[i] = new RendererData
{
renderer = realParts[i].GetComponentInChildren<Renderer>(),
propBlock = new MaterialPropertyBlock()
};
}
}
astralData = new RendererData[astralParts.Length];
for (int i = 0; i < astralParts.Length; i++)
{
if (astralParts[i])
{
astralData[i] = new RendererData
{
renderer = astralParts[i].GetComponentInChildren<Renderer>(),
propBlock = new MaterialPropertyBlock()
};
}
}
}
private IEnumerator CombinedTransitionRoutine()
{
isTransitioning = true;
int count = Mathf.Min(realParts.Length, astralParts.Length);
if (!isAstral && realBodyPrefab != null)
{
spawnedRealBody = Instantiate(realBodyPrefab, playerTransform.position, playerTransform.rotation);
Rigidbody rb = spawnedRealBody.GetComponent<Rigidbody>();
if (rb != null)
{
rb.useGravity = true;
rb.isKinematic = false;
rb.constraints = RigidbodyConstraints.FreezeRotationX | RigidbodyConstraints.FreezeRotationZ;
}
}
yield return StepPlayerForward();
yield return FadeToWhite(count);
isAstral = !isAstral;
for (int i = 0; i < count; i++)
{
SetRealPartActive(i, !isAstral);
SetAstralPartActive(i, isAstral);
}
yield return FadeToDimensionColors(count);
ResetColors();
isTransitioning = false;
}
private IEnumerator StepPlayerForward()
{
if (playerTransform == null) yield break;
Vector3 startPos = playerTransform.position;
Vector3 targetPos = startPos + (playerTransform.forward * forwardStepAmount);
float stepDuration = 0.2f;
float stepElapsed = 0f;
while (stepElapsed < stepDuration)
{
stepElapsed += Time.deltaTime;
float t = Mathf.SmoothStep(0f, 1f, stepElapsed / stepDuration);
playerTransform.position = Vector3.Lerp(startPos, targetPos, t);
yield return null;
}
playerTransform.position = targetPos;
}
private IEnumerator FadeToWhite(int count)
{
float timer = 0f;
while (timer < whitenDuration)
{
timer += Time.deltaTime;
float t = timer / whitenDuration;
Color pureWhite = Color.Lerp(Color.black, Color.white, t);
Color emissionBright = pureWhite * 0.5f;
if (bloom != null) bloom.intensity.value = Mathf.Lerp(2f, 4f, t);
ApplyColorToActiveParts(count, pureWhite, emissionBright);
yield return null;
}
}
private IEnumerator FadeToDimensionColors(int count)
{
float startBloom = bloom != null ? bloom.intensity.value : 2f;
float targetBloom = isAstral ? 3.5f : 2f;
float startVignette = vignette != null ? vignette.intensity.value : 0.2f;
float targetVignette = isAstral ? 0.45f : 0.2f;
Color startFilter = colorAdjustments != null ? colorAdjustments.colorFilter.value : Color.white;
Color targetFilter = isAstral ? new Color(0.8f, 0.9f, 1f) : Color.white;
Color targetBaseColor = isAstral ? new Color(0.85f, 0.9f, 0.95f) : Color.white;
Color targetEmissionColor = isAstral ? new Color(0.02f, 0.02f, 0.05f) : Color.black;
float timer = 0f;
while (timer < whitenDuration)
{
timer += Time.deltaTime;
float t = timer / whitenDuration;
if (colorAdjustments != null) colorAdjustments.colorFilter.value = Color.Lerp(startFilter, targetFilter, t);
if (vignette != null) vignette.intensity.value = Mathf.Lerp(startVignette, targetVignette, t);
if (bloom != null) bloom.intensity.value = Mathf.Lerp(startBloom, targetBloom, t);
Color fadeColor = Color.Lerp(Color.white, targetBaseColor, t);
Color emissionFade = Color.Lerp(Color.white, targetEmissionColor, t);
ApplyColorToActiveParts(count, fadeColor, emissionFade);
yield return null;
}
if (bloom != null) bloom.intensity.value = targetBloom;
if (vignette != null) vignette.intensity.value = targetVignette;
}
private void ApplyColorToActiveParts(int count, Color baseColor, Color emissionColor)
{
for (int i = 0; i < count; i++)
{
GameObject activeObj = isAstral ? astralParts[i] : realParts[i];
if (!activeObj || !activeObj.activeSelf) continue;
Renderer rend = isAstral ? astralData[i].renderer : realData[i].renderer;
MaterialPropertyBlock block = isAstral ? astralData[i].propBlock : realData[i].propBlock;
if (!rend) continue;
rend.GetPropertyBlock(block);
block.SetColor("_Color", baseColor);
block.SetColor("_BaseColor", baseColor);
block.SetColor("_EmissionColor", emissionColor);
rend.SetPropertyBlock(block);
}
}
private void ResetColors()
{
int count = Mathf.Min(realParts.Length, astralParts.Length);
Color targetBaseColor = isAstral ? new Color(0.6f, 0.7f, 0.8f) : Color.white;
Color targetEmissionColor = isAstral ? new Color(0.1f, 0.1f, 0.2f) : Color.black;
for (int i = 0; i < count; i++)
{
ApplyRendererColor(realData, i, targetBaseColor, targetEmissionColor);
ApplyRendererColor(astralData, i, targetBaseColor, targetEmissionColor);
}
}
private void ApplyRendererColor(RendererData[] data, int i, Color baseColor, Color emissionColor)
{
if (!data[i].renderer) return;
data[i].renderer.GetPropertyBlock(data[i].propBlock);
data[i].propBlock.SetColor("_Color", baseColor);
data[i].propBlock.SetColor("_BaseColor", baseColor);
data[i].propBlock.SetColor("_EmissionColor", emissionColor);
data[i].renderer.SetPropertyBlock(data[i].propBlock);
}
}