first commit
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class Door : MonoBehaviour
|
||||
{
|
||||
public Vector3 openOffset = Vector3.up * 3f;
|
||||
public float openSpeed = 2f;
|
||||
public bool isOpen;
|
||||
|
||||
private Vector3 closedPosition;
|
||||
private Vector3 targetPosition;
|
||||
private Collider doorCollider;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
closedPosition = transform.position;
|
||||
targetPosition = closedPosition;
|
||||
doorCollider = GetComponent<Collider>();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// MoveTowards даёт предсказуемое плавное открытие без физики и аниматора.
|
||||
transform.position = Vector3.MoveTowards(transform.position, targetPosition, openSpeed * Time.deltaTime);
|
||||
|
||||
if (isOpen && doorCollider != null && Vector3.Distance(transform.position, targetPosition) < 0.02f)
|
||||
{
|
||||
doorCollider.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Open()
|
||||
{
|
||||
isOpen = true;
|
||||
targetPosition = closedPosition + openOffset;
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
isOpen = false;
|
||||
targetPosition = closedPosition;
|
||||
|
||||
if (doorCollider != null)
|
||||
{
|
||||
doorCollider.enabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 849ebb2701cd89a3880164b587c2401f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,267 @@
|
||||
using UnityEngine;
|
||||
|
||||
[RequireComponent(typeof(Collider))]
|
||||
public class DraggableObject : MonoBehaviour
|
||||
{
|
||||
[Header("Item")]
|
||||
public ItemType itemType;
|
||||
public Material normalMaterial;
|
||||
public Material highlightMaterial;
|
||||
public Material dragMaterial;
|
||||
public Material inactiveMaterial;
|
||||
public Vector3 startPosition;
|
||||
public bool isLocked;
|
||||
|
||||
[Header("Drag")]
|
||||
public float dragLift = 0.35f;
|
||||
public float snapDistance = 1.25f;
|
||||
public bool isInteractable = true;
|
||||
|
||||
private Camera mainCamera;
|
||||
private Renderer itemRenderer;
|
||||
private Plane dragPlane;
|
||||
private Vector3 dragOffset;
|
||||
private Vector3 returnPosition;
|
||||
private Slot highlightedSlot;
|
||||
private bool isDragging;
|
||||
private bool isMouseOver;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
mainCamera = Camera.main;
|
||||
itemRenderer = GetComponent<Renderer>();
|
||||
startPosition = transform.position;
|
||||
returnPosition = startPosition;
|
||||
|
||||
if (itemRenderer != null && normalMaterial == null)
|
||||
{
|
||||
normalMaterial = itemRenderer.sharedMaterial;
|
||||
}
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
ApplyNormalMaterial();
|
||||
}
|
||||
|
||||
private void OnMouseEnter()
|
||||
{
|
||||
if (!CanInteract())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
isMouseOver = true;
|
||||
ApplyMaterial(highlightMaterial);
|
||||
}
|
||||
|
||||
private void OnMouseExit()
|
||||
{
|
||||
isMouseOver = false;
|
||||
|
||||
if (!isDragging && !isLocked)
|
||||
{
|
||||
ApplyNormalMaterial();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMouseDown()
|
||||
{
|
||||
if (!CanInteract())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (mainCamera == null)
|
||||
{
|
||||
mainCamera = Camera.main;
|
||||
}
|
||||
|
||||
// Запоминаем позицию перед перетаскиванием, чтобы вернуть предмет при ошибке.
|
||||
returnPosition = transform.position;
|
||||
dragPlane = new Plane(Vector3.up, new Vector3(0f, startPosition.y, 0f));
|
||||
|
||||
if (TryGetMousePointOnDragPlane(out Vector3 mouseWorldPoint))
|
||||
{
|
||||
dragOffset = transform.position - mouseWorldPoint;
|
||||
dragOffset.y = 0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
dragOffset = Vector3.zero;
|
||||
}
|
||||
|
||||
isDragging = true;
|
||||
ApplyMaterial(dragMaterial != null ? dragMaterial : highlightMaterial);
|
||||
}
|
||||
|
||||
private void OnMouseDrag()
|
||||
{
|
||||
if (!isDragging || !CanInteract())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TryGetMousePointOnDragPlane(out Vector3 mouseWorldPoint))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Offset сохраняет относительное положение курсора и предмета без резких скачков.
|
||||
Vector3 targetPosition = mouseWorldPoint + dragOffset;
|
||||
targetPosition.y = startPosition.y + dragLift;
|
||||
transform.position = targetPosition;
|
||||
|
||||
UpdateSlotPreview();
|
||||
}
|
||||
|
||||
private void OnMouseUp()
|
||||
{
|
||||
if (!isDragging)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
isDragging = false;
|
||||
// Предмет фиксируется только в ближайшем подходящем слоте.
|
||||
Slot nearestSlot = FindNearestSlot();
|
||||
ClearHighlightedSlot();
|
||||
|
||||
if (nearestSlot != null && nearestSlot.CanAccept(this))
|
||||
{
|
||||
nearestSlot.PlaceItem(this);
|
||||
return;
|
||||
}
|
||||
|
||||
transform.position = returnPosition;
|
||||
ApplyNormalMaterial();
|
||||
}
|
||||
|
||||
public void LockToSlot(Vector3 slotPosition)
|
||||
{
|
||||
isLocked = true;
|
||||
isInteractable = false;
|
||||
transform.position = slotPosition;
|
||||
ApplyNormalMaterial();
|
||||
}
|
||||
|
||||
public void SetInteractable(bool value)
|
||||
{
|
||||
if (isLocked)
|
||||
{
|
||||
isInteractable = false;
|
||||
return;
|
||||
}
|
||||
|
||||
isInteractable = value;
|
||||
ApplyNormalMaterial();
|
||||
}
|
||||
|
||||
public void SetNormalMaterial(Material material)
|
||||
{
|
||||
normalMaterial = material;
|
||||
ApplyNormalMaterial();
|
||||
}
|
||||
|
||||
private bool CanInteract()
|
||||
{
|
||||
return isInteractable && !isLocked;
|
||||
}
|
||||
|
||||
private bool TryGetMousePointOnDragPlane(out Vector3 worldPoint)
|
||||
{
|
||||
worldPoint = Vector3.zero;
|
||||
|
||||
if (mainCamera == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Raycast в математическую плоскость делает drag стабильным для ортографической камеры.
|
||||
Ray ray = mainCamera.ScreenPointToRay(Input.mousePosition);
|
||||
if (!dragPlane.Raycast(ray, out float distance))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
worldPoint = ray.GetPoint(distance);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void UpdateSlotPreview()
|
||||
{
|
||||
Slot nearestSlot = FindNearestSlot();
|
||||
|
||||
if (nearestSlot == highlightedSlot)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ClearHighlightedSlot();
|
||||
highlightedSlot = nearestSlot;
|
||||
|
||||
if (highlightedSlot != null)
|
||||
{
|
||||
highlightedSlot.ShowPreview(this);
|
||||
}
|
||||
}
|
||||
|
||||
private Slot FindNearestSlot()
|
||||
{
|
||||
Slot[] slots = FindObjectsOfType<Slot>();
|
||||
Slot nearest = null;
|
||||
float bestDistance = snapDistance;
|
||||
|
||||
foreach (Slot slot in slots)
|
||||
{
|
||||
float distance = Vector3.Distance(transform.position, slot.transform.position);
|
||||
if (distance <= bestDistance)
|
||||
{
|
||||
bestDistance = distance;
|
||||
nearest = slot;
|
||||
}
|
||||
}
|
||||
|
||||
return nearest;
|
||||
}
|
||||
|
||||
private void ClearHighlightedSlot()
|
||||
{
|
||||
if (highlightedSlot != null)
|
||||
{
|
||||
highlightedSlot.ClearPreview();
|
||||
highlightedSlot = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyNormalMaterial()
|
||||
{
|
||||
if (isLocked)
|
||||
{
|
||||
ApplyMaterial(normalMaterial);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isMouseOver && CanInteract())
|
||||
{
|
||||
ApplyMaterial(highlightMaterial);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isInteractable && inactiveMaterial != null)
|
||||
{
|
||||
ApplyMaterial(inactiveMaterial);
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyMaterial(normalMaterial);
|
||||
}
|
||||
|
||||
private void ApplyMaterial(Material material)
|
||||
{
|
||||
if (itemRenderer != null && material != null)
|
||||
{
|
||||
itemRenderer.sharedMaterial = material;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9ba685c5e754cd0adb5c37bc78754cb1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,7 @@
|
||||
public enum ItemType
|
||||
{
|
||||
Key,
|
||||
Gem,
|
||||
Cog,
|
||||
Battery
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4ebf65485a9a3e6c9a4cee39cb1cfaaa
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,79 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class PuzzleManager : MonoBehaviour
|
||||
{
|
||||
public bool keyPlaced;
|
||||
public bool gemPlaced;
|
||||
public bool puzzleSolved;
|
||||
|
||||
public PuzzleUI puzzleUI;
|
||||
public DraggableObject gemItem;
|
||||
|
||||
private const string KeyObjective = "Перетащите ключ в слот";
|
||||
private const string GemAvailableObjective = "Дверь открыта. Возьмите самоцвет за дверью";
|
||||
private const string GemObjective = "Перетащите самоцвет во второй слот";
|
||||
private const string SolvedObjective = "Пазл решён. Выход открыт";
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// Самоцвет становится доступен только после решения первого шага.
|
||||
if (gemItem != null)
|
||||
{
|
||||
gemItem.SetInteractable(false);
|
||||
}
|
||||
|
||||
if (puzzleUI != null)
|
||||
{
|
||||
puzzleUI.SetObjective(KeyObjective);
|
||||
puzzleUI.SetSolvedVisible(false);
|
||||
puzzleUI.UpdateDebug(keyPlaced, gemPlaced, puzzleSolved);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnKeyPlaced()
|
||||
{
|
||||
keyPlaced = true;
|
||||
|
||||
if (gemItem != null)
|
||||
{
|
||||
gemItem.SetInteractable(true);
|
||||
}
|
||||
|
||||
if (puzzleUI != null)
|
||||
{
|
||||
puzzleUI.SetObjective(GemAvailableObjective);
|
||||
puzzleUI.ShowMessage(GemObjective);
|
||||
}
|
||||
|
||||
CheckPuzzleSolved();
|
||||
}
|
||||
|
||||
public void OnGemPlaced()
|
||||
{
|
||||
gemPlaced = true;
|
||||
|
||||
if (puzzleUI != null)
|
||||
{
|
||||
puzzleUI.SetObjective(SolvedObjective);
|
||||
}
|
||||
|
||||
CheckPuzzleSolved();
|
||||
}
|
||||
|
||||
public void CheckPuzzleSolved()
|
||||
{
|
||||
// Финальное состояние наступает только после обоих правильных размещений.
|
||||
puzzleSolved = keyPlaced && gemPlaced;
|
||||
|
||||
if (puzzleUI != null)
|
||||
{
|
||||
puzzleUI.UpdateDebug(keyPlaced, gemPlaced, puzzleSolved);
|
||||
|
||||
if (puzzleSolved)
|
||||
{
|
||||
puzzleUI.SetSolvedVisible(true);
|
||||
puzzleUI.ShowMessage("Пазл решён!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6d6851990dab06249b1fa47ad466c337
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,62 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
public class PuzzleUI : MonoBehaviour
|
||||
{
|
||||
public Text objectiveText;
|
||||
public Text solvedText;
|
||||
public Text debugText;
|
||||
public Text messageText;
|
||||
|
||||
private float messageTimer;
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (messageTimer <= 0f || messageText == null || solvedText == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
messageTimer -= Time.deltaTime;
|
||||
|
||||
if (messageTimer <= 0f && !solvedText.gameObject.activeSelf)
|
||||
{
|
||||
messageText.text = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetObjective(string text)
|
||||
{
|
||||
if (objectiveText != null)
|
||||
{
|
||||
objectiveText.text = text;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetSolvedVisible(bool visible)
|
||||
{
|
||||
if (solvedText != null)
|
||||
{
|
||||
solvedText.gameObject.SetActive(visible);
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateDebug(bool keyPlaced, bool gemPlaced, bool solved)
|
||||
{
|
||||
if (debugText != null)
|
||||
{
|
||||
debugText.text = "Key placed: " + keyPlaced + "\n"
|
||||
+ "Gem placed: " + gemPlaced + "\n"
|
||||
+ "Puzzle solved: " + solved;
|
||||
}
|
||||
}
|
||||
|
||||
public void ShowMessage(string text)
|
||||
{
|
||||
if (messageText != null)
|
||||
{
|
||||
messageText.text = text;
|
||||
messageTimer = 3f;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: acd7a2645e9fbd10e9308e1f0ad16716
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,92 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
|
||||
[RequireComponent(typeof(Collider))]
|
||||
public class Slot : MonoBehaviour
|
||||
{
|
||||
public ItemType requiredItemType;
|
||||
public bool isOccupied;
|
||||
|
||||
[Header("Visuals")]
|
||||
public Material normalMaterial;
|
||||
public Material validPreviewMaterial;
|
||||
public Material invalidPreviewMaterial;
|
||||
public Material successMaterial;
|
||||
|
||||
[Header("Placement")]
|
||||
public Transform snapPoint;
|
||||
public UnityEvent onItemPlaced = new UnityEvent();
|
||||
|
||||
private Renderer slotRenderer;
|
||||
private DraggableObject placedItem;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
slotRenderer = GetComponent<Renderer>();
|
||||
|
||||
if (slotRenderer != null && normalMaterial == null)
|
||||
{
|
||||
normalMaterial = slotRenderer.sharedMaterial;
|
||||
}
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
ApplyMaterial(normalMaterial);
|
||||
}
|
||||
|
||||
public bool CanAccept(DraggableObject item)
|
||||
{
|
||||
return item != null && !isOccupied && item.itemType == requiredItemType;
|
||||
}
|
||||
|
||||
public void PlaceItem(DraggableObject item)
|
||||
{
|
||||
if (!CanAccept(item))
|
||||
{
|
||||
ShowPreview(item);
|
||||
return;
|
||||
}
|
||||
|
||||
isOccupied = true;
|
||||
placedItem = item;
|
||||
|
||||
// SnapPoint задаёт точку фиксации предмета над плоскостью слота.
|
||||
Vector3 targetPosition = snapPoint != null ? snapPoint.position : transform.position + Vector3.up * 0.35f;
|
||||
item.LockToSlot(targetPosition);
|
||||
|
||||
ApplyMaterial(successMaterial);
|
||||
onItemPlaced.Invoke();
|
||||
}
|
||||
|
||||
public void ResetSlot()
|
||||
{
|
||||
isOccupied = false;
|
||||
placedItem = null;
|
||||
ApplyMaterial(normalMaterial);
|
||||
}
|
||||
|
||||
public void ShowPreview(DraggableObject item)
|
||||
{
|
||||
if (isOccupied)
|
||||
{
|
||||
ApplyMaterial(successMaterial);
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyMaterial(CanAccept(item) ? validPreviewMaterial : invalidPreviewMaterial);
|
||||
}
|
||||
|
||||
public void ClearPreview()
|
||||
{
|
||||
ApplyMaterial(isOccupied ? successMaterial : normalMaterial);
|
||||
}
|
||||
|
||||
private void ApplyMaterial(Material material)
|
||||
{
|
||||
if (slotRenderer != null && material != null)
|
||||
{
|
||||
slotRenderer.sharedMaterial = material;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 968f7a479c33d6aa9a32a2b1176a06ac
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user