feat: add throwable boxs and set it as prefab

This commit is contained in:
Pierre Ryssen
2026-03-30 23:49:36 +02:00
871 changed files with 541814 additions and 229 deletions

View File

@@ -0,0 +1,189 @@
using System;
using UnityEngine;
using UnityEngine.Events;
public class ButtonSequenceDoorPuzzle : MonoBehaviour
{
[Header("References")]
[Tooltip("All available buttons for this puzzle.")]
[SerializeField] private WallInteractButton[] buttons;
[Tooltip("Door to open when the sequence is correct.")]
[SerializeField] private SlidingDoor targetDoor;
[Tooltip("Optional blocks controlled by this puzzle (reset with SetOff on start/wrong input).")]
[SerializeField] private TestBlock[] puzzleBlocks;
[Header("Sequence")]
[Tooltip("Button indices (from the buttons array) that must be pressed in order. Example: 2,0,3")]
[SerializeField] private int[] requiredSequence = { 0, 1, 2 };
[Tooltip("If true, wrong input resets progress back to 0.")]
[SerializeField] private bool resetOnWrongPress = true;
[Tooltip("If true, puzzle can only be solved once.")]
[SerializeField] private bool lockAfterSolved = true;
[Header("Debug")]
[SerializeField] private bool enableDebugLogs = true;
private int m_progress;
private bool m_isSolved;
private UnityAction[] m_cachedListeners;
private void OnEnable()
{
SetAllBlocksOff();
RegisterAllButtons();
}
private void OnDisable()
{
UnregisterAllButtons();
}
private void RegisterAllButtons()
{
if (buttons == null)
return;
m_cachedListeners = new UnityAction[buttons.Length];
for (int i = 0; i < buttons.Length; i++)
{
WallInteractButton button = buttons[i];
if (button == null)
continue;
int buttonIndex = i;
UnityAction action = () => OnButtonPressed(buttonIndex);
m_cachedListeners[i] = action;
button.OnInteract.AddListener(action);
}
}
private void UnregisterAllButtons()
{
if (buttons == null)
return;
for (int i = 0; i < buttons.Length; i++)
{
WallInteractButton button = buttons[i];
if (button == null)
continue;
if (m_cachedListeners != null && i < m_cachedListeners.Length && m_cachedListeners[i] != null)
button.OnInteract.RemoveListener(m_cachedListeners[i]);
}
m_cachedListeners = null;
m_progress = 0;
}
private void OnButtonPressed(int buttonIndex)
{
Log($"Button pressed: index {buttonIndex}");
if (m_isSolved && lockAfterSolved)
{
Log("Puzzle already solved and locked.");
return;
}
if (!IsSequenceValid())
{
Log("Invalid sequence configuration.");
return;
}
int expectedIndex = requiredSequence[m_progress];
Log($"Expected button index: {expectedIndex} (step {m_progress + 1}/{requiredSequence.Length})");
if (buttonIndex == expectedIndex)
{
m_progress++;
Log($"Correct input. Progress: {m_progress}/{requiredSequence.Length}");
if (m_progress >= requiredSequence.Length)
{
SolvePuzzle();
}
return;
}
if (resetOnWrongPress)
{
Log("Wrong input. Resetting sequence and turning puzzle blocks OFF.");
m_progress = 0;
SetAllBlocksOff();
return;
}
Log("Wrong input, but resetOnWrongPress is disabled.");
}
private bool IsSequenceValid()
{
if (requiredSequence == null || requiredSequence.Length == 0)
return false;
if (buttons == null || buttons.Length == 0)
return false;
for (int i = 0; i < requiredSequence.Length; i++)
{
int index = requiredSequence[i];
if (index < 0 || index >= buttons.Length)
return false;
}
return true;
}
private void SolvePuzzle()
{
m_isSolved = true;
m_progress = 0;
Log("Sequence completed. Opening door.");
if (targetDoor != null)
targetDoor.Open();
}
private void SetAllBlocksOff()
{
if (puzzleBlocks == null)
return;
for (int i = 0; i < puzzleBlocks.Length; i++)
{
if (puzzleBlocks[i] == null)
continue;
puzzleBlocks[i].SetOff();
}
}
private void Log(string message)
{
if (!enableDebugLogs)
return;
Debug.Log($"[{nameof(ButtonSequenceDoorPuzzle)}] {message}", this);
}
#if UNITY_EDITOR
private void OnValidate()
{
if (requiredSequence == null)
return;
for (int i = 0; i < requiredSequence.Length; i++)
{
requiredSequence[i] = Math.Max(0, requiredSequence[i]);
}
}
#endif
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 920802292ba9a49d2bee3519a905717d

View File

@@ -0,0 +1,72 @@
using UnityEngine;
public class BoxPickup : MonoBehaviour
{
public Transform PlayerTransform;
public Transform CameraTransform;
public Transform HandTransform;
public float ThrowForce = 10f;
public float PickupDistance = 5f;
private bool isHeld;
private Rigidbody m_rigidbody;
private PlayerInputController input;
void Start()
{
m_rigidbody = GetComponent<Rigidbody>();
input = PlayerTransform.GetComponent<PlayerInputController>();
}
void Update()
{
if (input.InteractPressed)
{
if (!isHeld)
TryPickup();
else
Drop();
}
if (input.ThrowPressed)
Throw();
}
private void TryPickup()
{
Collider[] hits = Physics.OverlapSphere(PlayerTransform.position, PickupDistance);
foreach (Collider hit in hits)
{
if (hit.transform == transform) {
Pickup();
return;
}
}
}
private void Pickup()
{
isHeld = true;
m_rigidbody.isKinematic = true;
transform.SetParent(HandTransform);
transform.localPosition = Vector3.zero;
transform.localRotation = Quaternion.identity;
}
private void Drop()
{
isHeld = false;
transform.SetParent(null);
m_rigidbody.isKinematic = false;
}
private void Throw()
{
if (!isHeld)
return;
Drop();
m_rigidbody.AddForce(PlayerTransform.forward * ThrowForce, ForceMode.Impulse);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a36af2e55a3732eb2abc110ae2365702

View File

@@ -0,0 +1,76 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
[RequireComponent(typeof(Collider))]
public class PressurePlateButton : MonoBehaviour
{
[Header("Detection")]
[Tooltip("Layers that can activate the plate. Keep default to allow everything.")]
[SerializeField] private LayerMask detectionMask = ~0;
[Tooltip("If true, rigidbody objects can activate the plate.")]
[SerializeField] private bool allowRigidbodies = true;
[Tooltip("If true, the Player can activate the plate (tag Player or PlayerMovement component).")]
[SerializeField] private bool allowPlayer = true;
[Header("Events")]
public UnityEvent OnPressed;
public UnityEvent OnReleased;
private readonly HashSet<Collider> m_validCollidersOnPlate = new HashSet<Collider>();
private bool m_isPressed;
private void Reset()
{
Collider col = GetComponent<Collider>();
col.isTrigger = true;
}
private void OnTriggerEnter(Collider other)
{
if (!IsValidActivator(other))
return;
m_validCollidersOnPlate.Add(other);
if (!m_isPressed)
{
m_isPressed = true;
OnPressed?.Invoke();
}
}
private void OnTriggerExit(Collider other)
{
if (!m_validCollidersOnPlate.Remove(other))
return;
if (m_validCollidersOnPlate.Count == 0 && m_isPressed)
{
m_isPressed = false;
OnReleased?.Invoke();
}
}
private bool IsValidActivator(Collider other)
{
if (((1 << other.gameObject.layer) & detectionMask) == 0)
return false;
if (allowPlayer)
{
if (other.CompareTag("Player"))
return true;
if (other.GetComponentInParent<PlayerMovement>() != null)
return true;
}
if (allowRigidbodies && other.attachedRigidbody != null)
return true;
return false;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 69374de63bb12844d97acb7a794b7b40

View File

@@ -0,0 +1,114 @@
using UnityEngine;
using UnityEngine.Events;
public class SlidingDoor : MonoBehaviour
{
public enum SlideAxis { X, Y, Z }
public enum SlideDirection { Positive = 1, Negative = -1 }
[Header("Slide Settings")]
[Tooltip("Local axis the door slides along.")]
[SerializeField] private SlideAxis axis = SlideAxis.X;
[Tooltip("Which way along the axis the door opens.")]
[SerializeField] private SlideDirection direction = SlideDirection.Positive;
[Tooltip("How far the door travels when fully open.")]
[SerializeField] private float slideDistance = 2f;
[Tooltip("Movement speed (units per second).")]
[SerializeField] private float speed = 3f;
[Header("Start State")]
[SerializeField] private bool startOpen = false;
[Header("Events")]
public UnityEvent OnOpened;
public UnityEvent OnClosed;
private Vector3 m_closedPos;
private Vector3 m_openPos;
private Vector3 m_targetPos;
private bool m_isOpen;
private bool m_eventFiredOpen;
private bool m_eventFiredClosed;
private void Awake()
{
m_closedPos = transform.localPosition;
m_openPos = m_closedPos + GetSlideVector() * slideDistance;
if (startOpen)
{
transform.localPosition = m_openPos;
m_isOpen = true;
}
m_targetPos = m_isOpen ? m_openPos : m_closedPos;
}
private void Update()
{
transform.localPosition = Vector3.MoveTowards(
transform.localPosition, m_targetPos, speed * Time.deltaTime);
if (Vector3.Distance(transform.localPosition, m_openPos) < 0.01f && !m_eventFiredOpen)
{
m_eventFiredOpen = true;
m_eventFiredClosed = false;
OnOpened?.Invoke();
}
else if (Vector3.Distance(transform.localPosition, m_closedPos) < 0.01f && !m_eventFiredClosed)
{
m_eventFiredClosed = true;
m_eventFiredOpen = false;
OnClosed?.Invoke();
}
}
public void Open()
{
m_isOpen = true;
m_targetPos = m_openPos;
}
public void Close()
{
m_isOpen = false;
m_targetPos = m_closedPos;
}
public void Toggle()
{
if (m_isOpen)
Close();
else
Open();
}
private Vector3 GetSlideVector()
{
float sign = (float)direction;
return axis switch
{
SlideAxis.X => new Vector3(sign, 0f, 0f),
SlideAxis.Y => new Vector3(0f, sign, 0f),
SlideAxis.Z => new Vector3(0f, 0f, sign),
_ => Vector3.right,
};
}
#if UNITY_EDITOR
private void OnDrawGizmos()
{
Vector3 worldClosed = transform.parent != null
? transform.parent.TransformPoint(transform.localPosition)
: transform.position;
Vector3 slideVec = transform.TransformDirection(GetSlideVector()) * slideDistance;
Gizmos.color = Color.cyan;
Gizmos.DrawLine(worldClosed, worldClosed + slideVec);
Gizmos.DrawWireSphere(worldClosed + slideVec, 0.08f);
}
#endif
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0cc6c36a261296f4c82e315da147ba93

View File

@@ -0,0 +1,148 @@
using UnityEngine;
using UnityEngine.Events;
public class TestBlock : MonoBehaviour
{
[Header("Visual")]
[SerializeField] private Renderer targetRenderer;
[SerializeField] private Color offColor = Color.red;
[SerializeField] private Color onColor = Color.green;
[Header("Material")]
[SerializeField] private string colorPropertyName = "_BaseColor";
[Header("Optional Auto Wiring")]
[Tooltip("If assigned, block turns ON while plate is pressed and OFF when released.")]
[SerializeField] private PressurePlateButton[] pressurePlates;
[Tooltip("If assigned, block toggles every time the wall button is interacted with.")]
[SerializeField] private WallInteractButton[] wallButtons;
private MaterialPropertyBlock m_propertyBlock;
private bool m_isOn;
private void Awake()
{
if (targetRenderer == null)
{
targetRenderer = GetComponentInChildren<Renderer>();
}
m_propertyBlock = new MaterialPropertyBlock();
}
private void Start()
{
SetOff();
}
private void OnEnable()
{
RegisterEvents();
}
private void OnDisable()
{
UnregisterEvents();
}
public void SetOn()
{
m_isOn = true;
Debug.Log($"{gameObject.name} turned ON");
ApplyColor(onColor);
}
public void SetOff()
{
m_isOn = false;
Debug.Log($"{gameObject.name} turned OFF");
ApplyColor(offColor);
}
public void Toggle()
{
if (m_isOn)
SetOff();
else
SetOn();
}
// Generic interaction entry point for UnityEvents from any interactable.
public void TriggerInteraction()
{
Toggle();
}
public void Activate()
{
SetOn();
}
public void Deactivate()
{
SetOff();
}
private void ApplyColor(Color color)
{
if (targetRenderer == null)
return;
targetRenderer.GetPropertyBlock(m_propertyBlock);
m_propertyBlock.SetColor(colorPropertyName, color);
targetRenderer.SetPropertyBlock(m_propertyBlock);
}
private void RegisterEvents()
{
if (pressurePlates != null)
{
for (int i = 0; i < pressurePlates.Length; i++)
{
if (pressurePlates[i] == null)
continue;
pressurePlates[i].OnPressed.AddListener(SetOn);
pressurePlates[i].OnReleased.AddListener(SetOff);
}
}
if (wallButtons != null)
{
for (int i = 0; i < wallButtons.Length; i++)
{
if (wallButtons[i] == null)
continue;
wallButtons[i].OnInteract.AddListener(Toggle);
}
}
}
private void UnregisterEvents()
{
if (pressurePlates != null)
{
for (int i = 0; i < pressurePlates.Length; i++)
{
if (pressurePlates[i] == null)
continue;
pressurePlates[i].OnPressed.RemoveListener(SetOn);
pressurePlates[i].OnReleased.RemoveListener(SetOff);
}
}
if (wallButtons != null)
{
for (int i = 0; i < wallButtons.Length; i++)
{
if (wallButtons[i] == null)
continue;
wallButtons[i].OnInteract.RemoveListener(Toggle);
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b93699191d7b58146b2c38540cbed40f

View File

@@ -0,0 +1,138 @@
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.InputSystem;
[RequireComponent(typeof(Collider))]
public class WallInteractButton : MonoBehaviour
{
[Header("Interaction")]
[Tooltip("Key the player must press to interact (keyboard fallback).")]
[SerializeField] private Key interactKey = Key.E;
[Tooltip("If true, the button can only be triggered once.")]
[SerializeField] private bool oneShot = false;
[Header("Physical Press")]
[Tooltip("Transform that moves to simulate a physical press (optional).")]
[SerializeField] private Transform buttonMesh;
[Tooltip("How far the button moves when pressed.")]
[SerializeField] private float pressDepth = 0.05f;
[Tooltip("Speed of the press/release animation.")]
[SerializeField] private float pressSpeed = 10f;
[Header("Events")]
public UnityEvent OnInteract;
private bool m_playerInRange;
private bool m_used;
private Vector3 m_buttonRestPos;
private Vector3 m_buttonPressedPos;
private bool m_isVisuallyPressed;
public PlayerHeadController headController;
private void Reset()
{
Collider col = GetComponent<Collider>();
col.isTrigger = true;
}
private void Start()
{
if (buttonMesh != null)
{
m_buttonRestPos = buttonMesh.localPosition;
m_buttonPressedPos = m_buttonRestPos - buttonMesh.localRotation * Vector3.forward * pressDepth;
}
}
private void Update()
{
if (!headController.isHoldingHead && m_playerInRange && Keyboard.current != null && Keyboard.current[interactKey].wasPressedThisFrame)
{
TryInteract();
}
AnimateButton();
}
private void TryInteract()
{
if (oneShot && m_used)
return;
m_used = true;
m_isVisuallyPressed = true;
OnInteract?.Invoke();
if (!oneShot)
Invoke(nameof(ReleaseVisual), 0.15f);
}
private void ReleaseVisual()
{
m_isVisuallyPressed = false;
}
private void AnimateButton()
{
if (buttonMesh == null)
return;
Vector3 target = m_isVisuallyPressed ? m_buttonPressedPos : m_buttonRestPos;
buttonMesh.localPosition = Vector3.Lerp(buttonMesh.localPosition, target, Time.deltaTime * pressSpeed);
}
private void OnTriggerEnter(Collider other)
{
if (IsPlayer(other))
m_playerInRange = true;
}
private void OnTriggerExit(Collider other)
{
if (IsPlayer(other))
m_playerInRange = false;
}
private bool IsPlayer(Collider other)
{
if (other.CompareTag("Player"))
return true;
if (other.GetComponentInParent<PlayerMovement>() != null)
return true;
return false;
}
#if UNITY_EDITOR
private void OnDrawGizmos()
{
Collider col = GetComponent<Collider>();
if (col == null)
return;
Gizmos.matrix = transform.localToWorldMatrix;
Gizmos.color = m_playerInRange
? new Color(0f, 1f, 0f, 0.25f)
: new Color(1f, 0.9f, 0f, 0.15f);
if (col is SphereCollider sphere)
{
Gizmos.DrawSphere(sphere.center, sphere.radius);
Gizmos.color = m_playerInRange ? Color.green : Color.yellow;
Gizmos.DrawWireSphere(sphere.center, sphere.radius);
}
else if (col is BoxCollider box)
{
Gizmos.DrawCube(box.center, box.size);
Gizmos.color = m_playerInRange ? Color.green : Color.yellow;
Gizmos.DrawWireCube(box.center, box.size);
}
}
#endif
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 96a3f628ff18ea34788167cc398180ca