bloak
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"permissions": {
|
"permissions": {
|
||||||
"allow": [
|
"allow": [
|
||||||
"WebSearch"
|
"WebSearch",
|
||||||
|
"WebFetch(domain:docs.unity3d.com)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8
Assets/Blocks.meta
Normal file
8
Assets/Blocks.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: aeacaeb9429d7f747bc5884f30e48a3d
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Assets/Blocks/Scripts.meta
Normal file
8
Assets/Blocks/Scripts.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 1ee7db48bfc98434ab8525bf0b140c62
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
30
Assets/Blocks/Scripts/BlockDefinition.cs
Normal file
30
Assets/Blocks/Scripts/BlockDefinition.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Описание одного типа блока.
|
||||||
|
/// Assets > Create > Blocks > Block Definition
|
||||||
|
/// </summary>
|
||||||
|
[CreateAssetMenu(fileName = "NewBlock", menuName = "Blocks/Block Definition")]
|
||||||
|
public class BlockDefinition : ScriptableObject
|
||||||
|
{
|
||||||
|
[Header("Identity")]
|
||||||
|
public string blockId; // уникальный ID, например "grass", "dirt"
|
||||||
|
public string displayName;
|
||||||
|
|
||||||
|
[Header("Textures (128×128)")]
|
||||||
|
public Texture2D topTexture; // грань Y+
|
||||||
|
public Texture2D bottomTexture; // грань Y-
|
||||||
|
public Texture2D sideTexture; // все 4 боковых грани
|
||||||
|
|
||||||
|
[Header("Inventory Link")]
|
||||||
|
[Tooltip("ItemData с типом Block, связанная с этим блоком")]
|
||||||
|
public ItemData linkedItem;
|
||||||
|
|
||||||
|
[Header("Properties")]
|
||||||
|
public bool isSolid = true; // false = стекло, вода — не кулируют соседей
|
||||||
|
|
||||||
|
// Индексы в Texture2DArray — заполняются BlockRegistry.Initialise()
|
||||||
|
[HideInInspector] public int topIndex = -1;
|
||||||
|
[HideInInspector] public int bottomIndex = -1;
|
||||||
|
[HideInInspector] public int sideIndex = -1;
|
||||||
|
}
|
||||||
2
Assets/Blocks/Scripts/BlockDefinition.cs.meta
Normal file
2
Assets/Blocks/Scripts/BlockDefinition.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 0c5e75bc3cec52e44bb5bcb6687e6237
|
||||||
188
Assets/Blocks/Scripts/BlockPlacer.cs
Normal file
188
Assets/Blocks/Scripts/BlockPlacer.cs
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Компонент на игроке.
|
||||||
|
/// ЛКМ — удержание → разрушение блока с прогрессом.
|
||||||
|
/// ПКМ — мгновенная постановка блока из хотбара.
|
||||||
|
/// </summary>
|
||||||
|
public class BlockPlacer : MonoBehaviour
|
||||||
|
{
|
||||||
|
[Header("Settings")]
|
||||||
|
public float reach = 5f; // дальность в Unity-единицах
|
||||||
|
public float breakTime = 1f; // секунд до разрушения
|
||||||
|
public LayerMask blockLayer; // слой чанков (назначить в Inspector)
|
||||||
|
|
||||||
|
[Header("Break Overlay")]
|
||||||
|
public Material breakMaterial; // Blocks/BlockBreak шейдер
|
||||||
|
public Texture2D crackTexture; // текстура трещин (grayscale)
|
||||||
|
|
||||||
|
// ── Приватное ─────────────────────────────────────────────────────────────
|
||||||
|
Camera _cam;
|
||||||
|
float _breakProgress;
|
||||||
|
Vector3Int _breakingBlock = Vector3Int.one * int.MinValue; // "никакой"
|
||||||
|
GameObject _breakOverlay;
|
||||||
|
MeshRenderer _overlayRenderer;
|
||||||
|
|
||||||
|
static readonly int PropProgress = Shader.PropertyToID("_Progress");
|
||||||
|
|
||||||
|
void Start()
|
||||||
|
{
|
||||||
|
_cam = Camera.main;
|
||||||
|
|
||||||
|
if (breakMaterial == null)
|
||||||
|
{
|
||||||
|
var shader = Shader.Find("Blocks/BlockBreak");
|
||||||
|
if (shader != null) breakMaterial = new Material(shader);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (breakMaterial != null && crackTexture != null)
|
||||||
|
breakMaterial.SetTexture("_CrackTex", crackTexture);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Update()
|
||||||
|
{
|
||||||
|
HandleBreak();
|
||||||
|
HandlePlace();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Разрушение (ЛКМ удержание) ───────────────────────────────────────────
|
||||||
|
|
||||||
|
void HandleBreak()
|
||||||
|
{
|
||||||
|
if (!Input.GetMouseButton(0))
|
||||||
|
{
|
||||||
|
CancelBreak();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Raycast(out RaycastHit hit, out Vector3Int blockPos))
|
||||||
|
{
|
||||||
|
CancelBreak();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сменился ли целевой блок?
|
||||||
|
if (blockPos != _breakingBlock)
|
||||||
|
{
|
||||||
|
CancelBreak();
|
||||||
|
_breakingBlock = blockPos;
|
||||||
|
SpawnBreakOverlay(blockPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
_breakProgress += Time.deltaTime / breakTime;
|
||||||
|
UpdateOverlay(_breakProgress);
|
||||||
|
|
||||||
|
if (_breakProgress >= 1f)
|
||||||
|
{
|
||||||
|
FinishBreak(blockPos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FinishBreak(Vector3Int blockPos)
|
||||||
|
{
|
||||||
|
string blockId = WorldManager.Instance.BreakBlock(blockPos);
|
||||||
|
CancelBreak();
|
||||||
|
|
||||||
|
if (blockId == null) return;
|
||||||
|
|
||||||
|
// Дроп предмета
|
||||||
|
var def = BlockRegistry.Instance?.GetById(blockId);
|
||||||
|
if (def?.linkedItem != null && InventoryItemFactory.Instance != null)
|
||||||
|
{
|
||||||
|
Vector3 dropPos = new Vector3(blockPos.x + 0.5f, blockPos.y + 0.5f, blockPos.z + 0.5f);
|
||||||
|
InventoryItemFactory.Instance.CreateWorldItem(def.linkedItem, 1, dropPos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CancelBreak()
|
||||||
|
{
|
||||||
|
_breakProgress = 0f;
|
||||||
|
_breakingBlock = Vector3Int.one * int.MinValue;
|
||||||
|
if (_breakOverlay != null) Destroy(_breakOverlay);
|
||||||
|
_breakOverlay = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Постановка (ПКМ) ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
void HandlePlace()
|
||||||
|
{
|
||||||
|
if (!Input.GetMouseButtonDown(1)) return;
|
||||||
|
if (!Raycast(out RaycastHit hit, out Vector3Int blockPos)) return;
|
||||||
|
|
||||||
|
// Ячейка, куда ставим = сосед грани которую мы смотрим
|
||||||
|
Vector3Int placePos = blockPos + Vector3Int.RoundToInt(hit.normal);
|
||||||
|
|
||||||
|
// Не ставим блок внутри игрока
|
||||||
|
if (IsInsidePlayer(placePos)) return;
|
||||||
|
|
||||||
|
// Получаем блок из хотбара
|
||||||
|
var selectedItem = InventoryManager.Instance?.GetSelectedHotbarItem();
|
||||||
|
if (selectedItem == null) return;
|
||||||
|
|
||||||
|
var def = BlockRegistry.Instance?.GetByItemId(selectedItem.id);
|
||||||
|
if (def == null)
|
||||||
|
{
|
||||||
|
Debug.Log($"[BlockPlacer] {selectedItem.itemName} не является блоком.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
WorldManager.Instance.PlaceBlock(placePos, def.blockId);
|
||||||
|
|
||||||
|
// Потребляем 1 из хотбара
|
||||||
|
InventoryManager.Instance.RemoveItem(selectedItem.id, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Overlay разрушения ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
void SpawnBreakOverlay(Vector3Int blockPos)
|
||||||
|
{
|
||||||
|
if (breakMaterial == null) return;
|
||||||
|
|
||||||
|
_breakOverlay = GameObject.CreatePrimitive(PrimitiveType.Cube);
|
||||||
|
_breakOverlay.name = "BreakOverlay";
|
||||||
|
_breakOverlay.transform.position = blockPos + new Vector3(0.5f, 0.5f, 0.5f);
|
||||||
|
_breakOverlay.transform.localScale = Vector3.one * 1.001f; // чуть больше, без z-fight
|
||||||
|
|
||||||
|
// Убираем коллайдер — он не нужен
|
||||||
|
Destroy(_breakOverlay.GetComponent<Collider>());
|
||||||
|
|
||||||
|
_overlayRenderer = _breakOverlay.GetComponent<MeshRenderer>();
|
||||||
|
_overlayRenderer.sharedMaterial = breakMaterial;
|
||||||
|
_overlayRenderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateOverlay(float progress)
|
||||||
|
{
|
||||||
|
if (_overlayRenderer == null) return;
|
||||||
|
// MaterialPropertyBlock — не создаём новый экземпляр материала
|
||||||
|
var mpb = new MaterialPropertyBlock();
|
||||||
|
_overlayRenderer.GetPropertyBlock(mpb);
|
||||||
|
mpb.SetFloat(PropProgress, progress);
|
||||||
|
_overlayRenderer.SetPropertyBlock(mpb);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Утилиты ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
bool Raycast(out RaycastHit hit, out Vector3Int blockPos)
|
||||||
|
{
|
||||||
|
blockPos = default;
|
||||||
|
Ray ray = new Ray(_cam.transform.position, _cam.transform.forward);
|
||||||
|
|
||||||
|
if (Physics.Raycast(ray, out hit, reach, blockLayer))
|
||||||
|
{
|
||||||
|
// hit.point лежит на грани — смещаемся чуть внутрь блока
|
||||||
|
Vector3 inner = hit.point - hit.normal * 0.01f;
|
||||||
|
blockPos = WorldManager.WorldToBlock(inner);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsInsidePlayer(Vector3Int blockPos)
|
||||||
|
{
|
||||||
|
// Проверяем две ячейки (ноги и голова)
|
||||||
|
Vector3Int feet = WorldManager.WorldToBlock(transform.position);
|
||||||
|
Vector3Int head = WorldManager.WorldToBlock(transform.position + Vector3.up * 1.8f);
|
||||||
|
return blockPos == feet || blockPos == head;
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/Blocks/Scripts/BlockPlacer.cs.meta
Normal file
2
Assets/Blocks/Scripts/BlockPlacer.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f550906f577c6db498f5d7923f708fbb
|
||||||
139
Assets/Blocks/Scripts/BlockRegistry.cs
Normal file
139
Assets/Blocks/Scripts/BlockRegistry.cs
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Синглтон. Собирает все BlockDefinition, строит Texture2DArray,
|
||||||
|
/// создаёт один shared Material для всех чанков.
|
||||||
|
/// Вызвать Initialise() до создания любого чанка.
|
||||||
|
/// </summary>
|
||||||
|
public class BlockRegistry : MonoBehaviour
|
||||||
|
{
|
||||||
|
public static BlockRegistry Instance { get; private set; }
|
||||||
|
|
||||||
|
[Header("All block definitions in the game")]
|
||||||
|
public List<BlockDefinition> definitions = new();
|
||||||
|
|
||||||
|
[Header("Shader")]
|
||||||
|
public Shader voxelShader; // назначить Blocks/VoxelLit через Inspector
|
||||||
|
|
||||||
|
// Доступно после Initialise()
|
||||||
|
public Material ChunkMaterial { get; private set; }
|
||||||
|
public Texture2DArray TexArray { get; private set; }
|
||||||
|
|
||||||
|
private readonly Dictionary<string, BlockDefinition> _byId = new();
|
||||||
|
private readonly Dictionary<string, BlockDefinition> _byItemId = new();
|
||||||
|
|
||||||
|
void Awake()
|
||||||
|
{
|
||||||
|
if (Instance != null) { Destroy(gameObject); return; }
|
||||||
|
Instance = this;
|
||||||
|
DontDestroyOnLoad(gameObject);
|
||||||
|
Initialise();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Initialise()
|
||||||
|
{
|
||||||
|
_byId.Clear();
|
||||||
|
_byItemId.Clear();
|
||||||
|
|
||||||
|
if (definitions == null || definitions.Count == 0)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[BlockRegistry] No BlockDefinitions assigned.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Собираем уникальные текстуры (top/bottom/side каждого блока)
|
||||||
|
// Порядок: для блока i → top=i*3, bottom=i*3+1, side=i*3+2
|
||||||
|
int layerCount = definitions.Count * 3;
|
||||||
|
int res = 128; // разрешение должно совпадать с текстурами блоков
|
||||||
|
|
||||||
|
TexArray = new Texture2DArray(res, res, layerCount,
|
||||||
|
TextureFormat.RGBA32, true, false);
|
||||||
|
TexArray.filterMode = FilterMode.Point; // пиксельный стиль
|
||||||
|
TexArray.wrapMode = TextureWrapMode.Repeat;
|
||||||
|
|
||||||
|
for (int i = 0; i < definitions.Count; i++)
|
||||||
|
{
|
||||||
|
var def = definitions[i];
|
||||||
|
|
||||||
|
def.topIndex = i * 3;
|
||||||
|
def.bottomIndex = i * 3 + 1;
|
||||||
|
def.sideIndex = i * 3 + 2;
|
||||||
|
|
||||||
|
CopyTexture(def.topTexture, TexArray, def.topIndex, res);
|
||||||
|
CopyTexture(def.bottomTexture, TexArray, def.bottomIndex, res);
|
||||||
|
CopyTexture(def.sideTexture, TexArray, def.sideIndex, res);
|
||||||
|
|
||||||
|
_byId[def.blockId] = def;
|
||||||
|
|
||||||
|
if (def.linkedItem != null)
|
||||||
|
_byItemId[def.linkedItem.id] = def;
|
||||||
|
}
|
||||||
|
|
||||||
|
TexArray.Apply(true, true); // загрузить на GPU, сделать non-readable
|
||||||
|
|
||||||
|
// Создаём один материал для всех чанков
|
||||||
|
if (voxelShader == null)
|
||||||
|
voxelShader = Shader.Find("Blocks/VoxelLit");
|
||||||
|
|
||||||
|
ChunkMaterial = new Material(voxelShader);
|
||||||
|
ChunkMaterial.SetTexture("_MainTexArray", TexArray);
|
||||||
|
|
||||||
|
Debug.Log($"[BlockRegistry] Initialised {definitions.Count} blocks, {layerCount} texture layers.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Lookups ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public BlockDefinition GetById(string blockId)
|
||||||
|
{
|
||||||
|
_byId.TryGetValue(blockId, out var def);
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Находит BlockDefinition по id связанного ItemData (для постановки блока).</summary>
|
||||||
|
public BlockDefinition GetByItemId(string itemDataId)
|
||||||
|
{
|
||||||
|
_byItemId.TryGetValue(itemDataId, out var def);
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Утилиты ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Копирует пиксели из src в Texture2DArray на слой layerIndex.
|
||||||
|
/// Если src null или не того размера — заполняет пурпурным (ошибка-текстура).
|
||||||
|
/// </summary>
|
||||||
|
static void CopyTexture(Texture2D src, Texture2DArray dst, int layerIndex, int res)
|
||||||
|
{
|
||||||
|
Color[] pixels;
|
||||||
|
|
||||||
|
if (src == null)
|
||||||
|
{
|
||||||
|
// Пурпурный = "текстура не назначена"
|
||||||
|
pixels = new Color[res * res];
|
||||||
|
for (int p = 0; p < pixels.Length; p++)
|
||||||
|
pixels[p] = (p / res + p) % 2 == 0 ? Color.magenta : Color.black;
|
||||||
|
}
|
||||||
|
else if (src.width != res || src.height != res)
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"[BlockRegistry] {src.name} is {src.width}×{src.height}, expected {res}×{res}. Scaling.");
|
||||||
|
// Простой fallback — масштабировать через RenderTexture
|
||||||
|
var rt = RenderTexture.GetTemporary(res, res);
|
||||||
|
Graphics.Blit(src, rt);
|
||||||
|
var prev = RenderTexture.active;
|
||||||
|
RenderTexture.active = rt;
|
||||||
|
var resized = new Texture2D(res, res, TextureFormat.RGBA32, false);
|
||||||
|
resized.ReadPixels(new Rect(0, 0, res, res), 0, 0);
|
||||||
|
resized.Apply();
|
||||||
|
RenderTexture.active = prev;
|
||||||
|
RenderTexture.ReleaseTemporary(rt);
|
||||||
|
pixels = resized.GetPixels();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
pixels = src.GetPixels();
|
||||||
|
}
|
||||||
|
|
||||||
|
dst.SetPixels(pixels, layerIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/Blocks/Scripts/BlockRegistry.cs.meta
Normal file
2
Assets/Blocks/Scripts/BlockRegistry.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 68a1a0f0ab870014ba9a973042ee35b4
|
||||||
180
Assets/Blocks/Scripts/Chunk.cs
Normal file
180
Assets/Blocks/Scripts/Chunk.cs
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MonoBehaviour-обёртка над ChunkData.
|
||||||
|
/// Хранит данные блоков и собирает процедурный меш с face culling.
|
||||||
|
/// </summary>
|
||||||
|
[RequireComponent(typeof(MeshFilter))]
|
||||||
|
[RequireComponent(typeof(MeshRenderer))]
|
||||||
|
[RequireComponent(typeof(MeshCollider))]
|
||||||
|
public class Chunk : MonoBehaviour
|
||||||
|
{
|
||||||
|
public ChunkData Data { get; private set; }
|
||||||
|
|
||||||
|
MeshFilter _filter;
|
||||||
|
MeshRenderer _renderer;
|
||||||
|
MeshCollider _collider;
|
||||||
|
Mesh _mesh;
|
||||||
|
|
||||||
|
// ── 6 направлений граней ──────────────────────────────────────────────────
|
||||||
|
// Порядок: Top, Bottom, Front(Z+), Back(Z-), Right(X+), Left(X-)
|
||||||
|
static readonly Vector3Int[] Directions =
|
||||||
|
{
|
||||||
|
Vector3Int.up, Vector3Int.down,
|
||||||
|
new(0,0,1), new(0,0,-1),
|
||||||
|
new(1,0,0), new(-1,0,0)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Вершины каждой грани (относительно нижнего-левого угла блока)
|
||||||
|
// Порядок по часовой стрелке, смотрим снаружи
|
||||||
|
static readonly Vector3[][] FaceVerts =
|
||||||
|
{
|
||||||
|
// Top (Y+)
|
||||||
|
new[] { new Vector3(0,1,0), new Vector3(0,1,1), new Vector3(1,1,1), new Vector3(1,1,0) },
|
||||||
|
// Bottom (Y-)
|
||||||
|
new[] { new Vector3(0,0,0), new Vector3(1,0,0), new Vector3(1,0,1), new Vector3(0,0,1) },
|
||||||
|
// Front (Z+)
|
||||||
|
new[] { new Vector3(1,0,1), new Vector3(0,0,1), new Vector3(0,1,1), new Vector3(1,1,1) },
|
||||||
|
// Back (Z-)
|
||||||
|
new[] { new Vector3(0,0,0), new Vector3(1,0,0), new Vector3(1,1,0), new Vector3(0,1,0) },
|
||||||
|
// Right (X+)
|
||||||
|
new[] { new Vector3(1,0,0), new Vector3(1,0,1), new Vector3(1,1,1), new Vector3(1,1,0) },
|
||||||
|
// Left (X-)
|
||||||
|
new[] { new Vector3(0,0,1), new Vector3(0,0,0), new Vector3(0,1,0), new Vector3(0,1,1) }
|
||||||
|
};
|
||||||
|
|
||||||
|
// UV для каждой из 4 вершин грани
|
||||||
|
static readonly Vector2[] FaceUVs =
|
||||||
|
{
|
||||||
|
new(0,0), new(1,0), new(1,1), new(0,1)
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Инициализация ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
void Awake()
|
||||||
|
{
|
||||||
|
_filter = GetComponent<MeshFilter>();
|
||||||
|
_renderer = GetComponent<MeshRenderer>();
|
||||||
|
_collider = GetComponent<MeshCollider>();
|
||||||
|
|
||||||
|
_mesh = new Mesh { name = "ChunkMesh" };
|
||||||
|
_mesh.MarkDynamic(); // оптимизация для часто изменяемых мешей
|
||||||
|
_filter.sharedMesh = _mesh;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Initialise(ChunkData data, Material chunkMaterial)
|
||||||
|
{
|
||||||
|
Data = data;
|
||||||
|
_renderer.sharedMaterial = chunkMaterial;
|
||||||
|
RebuildMesh();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Публичный API ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public void SetBlock(Vector3Int localPos, string blockId)
|
||||||
|
{
|
||||||
|
Data.SetBlock(localPos, blockId);
|
||||||
|
RebuildMesh();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveBlock(Vector3Int localPos)
|
||||||
|
{
|
||||||
|
Data.SetBlock(localPos, null);
|
||||||
|
RebuildMesh();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetBlock(Vector3Int localPos) => Data.GetBlock(localPos);
|
||||||
|
|
||||||
|
// ── Построение меша ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public void RebuildMesh()
|
||||||
|
{
|
||||||
|
if (Data == null) return;
|
||||||
|
|
||||||
|
var verts = new List<Vector3>();
|
||||||
|
var tris = new List<int>();
|
||||||
|
var uvs = new List<Vector2>(); // UV0: координаты на грани 0-1
|
||||||
|
var texCoord1 = new List<Vector2>(); // UV1: x = индекс в Texture2DArray
|
||||||
|
|
||||||
|
for (int x = 0; x < ChunkData.SIZE; x++)
|
||||||
|
for (int y = 0; y < ChunkData.HEIGHT; y++)
|
||||||
|
for (int z = 0; z < ChunkData.SIZE; z++)
|
||||||
|
{
|
||||||
|
string blockId = Data.GetBlock(x, y, z);
|
||||||
|
if (blockId == null) continue;
|
||||||
|
|
||||||
|
var def = BlockRegistry.Instance?.GetById(blockId);
|
||||||
|
if (def == null) continue;
|
||||||
|
|
||||||
|
for (int f = 0; f < 6; f++)
|
||||||
|
{
|
||||||
|
Vector3Int neighbour = new Vector3Int(x, y, z) + Directions[f];
|
||||||
|
|
||||||
|
// Face culling: пропускаем грань если сосед — твёрдый блок
|
||||||
|
if (HasSolidNeighbour(neighbour)) continue;
|
||||||
|
|
||||||
|
// Индекс текстуры для этой грани
|
||||||
|
int texIndex = f == 0 ? def.topIndex :
|
||||||
|
f == 1 ? def.bottomIndex :
|
||||||
|
def.sideIndex;
|
||||||
|
|
||||||
|
int baseVert = verts.Count;
|
||||||
|
var origin = new Vector3(x, y, z);
|
||||||
|
|
||||||
|
for (int v = 0; v < 4; v++)
|
||||||
|
{
|
||||||
|
verts.Add(origin + FaceVerts[f][v]);
|
||||||
|
uvs.Add(FaceUVs[v]);
|
||||||
|
texCoord1.Add(new Vector2(texIndex, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Два треугольника на грань
|
||||||
|
tris.Add(baseVert + 0);
|
||||||
|
tris.Add(baseVert + 1);
|
||||||
|
tris.Add(baseVert + 2);
|
||||||
|
tris.Add(baseVert + 0);
|
||||||
|
tris.Add(baseVert + 2);
|
||||||
|
tris.Add(baseVert + 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_mesh.Clear();
|
||||||
|
_mesh.SetVertices(verts);
|
||||||
|
_mesh.SetTriangles(tris, 0);
|
||||||
|
_mesh.SetUVs(0, uvs);
|
||||||
|
_mesh.SetUVs(1, texCoord1);
|
||||||
|
_mesh.RecalculateNormals();
|
||||||
|
_mesh.RecalculateBounds();
|
||||||
|
|
||||||
|
// Обновляем коллайдер (нужен для рейкаста)
|
||||||
|
_collider.sharedMesh = null;
|
||||||
|
_collider.sharedMesh = _mesh;
|
||||||
|
|
||||||
|
Data.ClearDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Face culling ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
bool HasSolidNeighbour(Vector3Int localPos)
|
||||||
|
{
|
||||||
|
// Сосед внутри этого чанка
|
||||||
|
if (ChunkData.InBounds(localPos.x, localPos.y, localPos.z))
|
||||||
|
{
|
||||||
|
string id = Data.GetBlock(localPos);
|
||||||
|
if (id == null) return false;
|
||||||
|
var def = BlockRegistry.Instance?.GetById(id);
|
||||||
|
return def != null && def.isSolid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сосед в соседнем чанке — спрашиваем WorldManager
|
||||||
|
if (WorldManager.Instance == null) return false;
|
||||||
|
|
||||||
|
Vector3Int worldOrigin = ChunkData.ChunkToWorld(Data.ChunkPos);
|
||||||
|
Vector3Int worldNeighbour = worldOrigin + localPos;
|
||||||
|
string neighbourId = WorldManager.Instance.GetBlock(worldNeighbour);
|
||||||
|
if (neighbourId == null) return false;
|
||||||
|
var neighbourDef = BlockRegistry.Instance?.GetById(neighbourId);
|
||||||
|
return neighbourDef != null && neighbourDef.isSolid;
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/Blocks/Scripts/Chunk.cs.meta
Normal file
2
Assets/Blocks/Scripts/Chunk.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ea3aeaf8d207ac642ba903796f27e9fc
|
||||||
78
Assets/Blocks/Scripts/ChunkData.cs
Normal file
78
Assets/Blocks/Scripts/ChunkData.cs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Чистые данные чанка — без MonoBehaviour, без Unity-объектов.
|
||||||
|
/// Легко сериализовать, передать в генератор, сохранить на диск.
|
||||||
|
/// </summary>
|
||||||
|
public class ChunkData
|
||||||
|
{
|
||||||
|
public const int SIZE = 16; // блоков по X и Z
|
||||||
|
public const int HEIGHT = 16; // блоков по Y
|
||||||
|
|
||||||
|
public readonly Vector3Int ChunkPos; // позиция чанка в чанковых координатах
|
||||||
|
|
||||||
|
// null = пустой блок, иначе blockId
|
||||||
|
private readonly string[,,] _blocks = new string[SIZE, HEIGHT, SIZE];
|
||||||
|
|
||||||
|
public bool IsDirty { get; private set; } = true; // нужен ли пересчёт меша
|
||||||
|
|
||||||
|
public ChunkData(Vector3Int chunkPos)
|
||||||
|
{
|
||||||
|
ChunkPos = chunkPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Получить blockId по локальным координатам. null = пусто.</summary>
|
||||||
|
public string GetBlock(int x, int y, int z)
|
||||||
|
{
|
||||||
|
if (!InBounds(x, y, z)) return null;
|
||||||
|
return _blocks[x, y, z];
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetBlock(Vector3Int local) => GetBlock(local.x, local.y, local.z);
|
||||||
|
|
||||||
|
/// <summary>Установить блок. null = удалить.</summary>
|
||||||
|
public void SetBlock(int x, int y, int z, string blockId)
|
||||||
|
{
|
||||||
|
if (!InBounds(x, y, z)) return;
|
||||||
|
_blocks[x, y, z] = blockId;
|
||||||
|
IsDirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetBlock(Vector3Int local, string blockId) =>
|
||||||
|
SetBlock(local.x, local.y, local.z, blockId);
|
||||||
|
|
||||||
|
public void ClearDirty() => IsDirty = false;
|
||||||
|
|
||||||
|
public static bool InBounds(int x, int y, int z) =>
|
||||||
|
x >= 0 && x < SIZE &&
|
||||||
|
y >= 0 && y < HEIGHT &&
|
||||||
|
z >= 0 && z < SIZE;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Перевод мировой позиции блока → позиция чанка (чанковые координаты).
|
||||||
|
/// </summary>
|
||||||
|
public static Vector3Int WorldToChunkPos(Vector3Int worldBlock) =>
|
||||||
|
new Vector3Int(
|
||||||
|
Mathf.FloorToInt((float)worldBlock.x / SIZE),
|
||||||
|
Mathf.FloorToInt((float)worldBlock.y / HEIGHT),
|
||||||
|
Mathf.FloorToInt((float)worldBlock.z / SIZE));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Перевод мировой позиции блока → локальная позиция внутри чанка.
|
||||||
|
/// </summary>
|
||||||
|
public static Vector3Int WorldToLocal(Vector3Int worldBlock)
|
||||||
|
{
|
||||||
|
int x = worldBlock.x % SIZE;
|
||||||
|
int y = worldBlock.y % HEIGHT;
|
||||||
|
int z = worldBlock.z % SIZE;
|
||||||
|
// Обработка отрицательных координат
|
||||||
|
if (x < 0) x += SIZE;
|
||||||
|
if (y < 0) y += HEIGHT;
|
||||||
|
if (z < 0) z += SIZE;
|
||||||
|
return new Vector3Int(x, y, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Позиция чанка → мировая позиция его нижнего-левого угла.</summary>
|
||||||
|
public static Vector3Int ChunkToWorld(Vector3Int chunkPos) =>
|
||||||
|
new Vector3Int(chunkPos.x * SIZE, chunkPos.y * HEIGHT, chunkPos.z * SIZE);
|
||||||
|
}
|
||||||
2
Assets/Blocks/Scripts/ChunkData.cs.meta
Normal file
2
Assets/Blocks/Scripts/ChunkData.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 3810cfa528149d9438c4fa8efe5f6611
|
||||||
8
Assets/Blocks/Scripts/Editor.meta
Normal file
8
Assets/Blocks/Scripts/Editor.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 223008930405c524fa586a9b9018ef71
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
104
Assets/Blocks/Scripts/Editor/BlockSetupTool.cs
Normal file
104
Assets/Blocks/Scripts/Editor/BlockSetupTool.cs
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tools > Block World Setup — добавляет все нужные объекты в сцену.
|
||||||
|
/// </summary>
|
||||||
|
public static class BlockSetupTool
|
||||||
|
{
|
||||||
|
[MenuItem("Tools/Block World Setup")]
|
||||||
|
public static void Setup()
|
||||||
|
{
|
||||||
|
// ── BlockRegistry ────────────────────────────────────────────────────
|
||||||
|
var registry = Object.FindObjectOfType<BlockRegistry>();
|
||||||
|
if (registry == null)
|
||||||
|
{
|
||||||
|
var go = new GameObject("BlockRegistry");
|
||||||
|
registry = go.AddComponent<BlockRegistry>();
|
||||||
|
|
||||||
|
var shader = Shader.Find("Blocks/VoxelLit");
|
||||||
|
if (shader != null) registry.voxelShader = shader;
|
||||||
|
else Debug.LogWarning("[BlockSetup] Shader 'Blocks/VoxelLit' not found. Assign manually.");
|
||||||
|
|
||||||
|
Debug.Log("[BlockSetup] Created BlockRegistry.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── WorldManager ──────────────────────────────────────────────────────
|
||||||
|
var wm = Object.FindObjectOfType<WorldManager>();
|
||||||
|
if (wm == null)
|
||||||
|
{
|
||||||
|
var go = new GameObject("WorldManager");
|
||||||
|
wm = go.AddComponent<WorldManager>();
|
||||||
|
wm.registry = registry;
|
||||||
|
Debug.Log("[BlockSetup] Created WorldManager.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── FlatWorldGenerator ────────────────────────────────────────────────
|
||||||
|
var gen = Object.FindObjectOfType<FlatWorldGenerator>();
|
||||||
|
if (gen == null)
|
||||||
|
{
|
||||||
|
var go = new GameObject("FlatWorldGenerator");
|
||||||
|
gen = go.AddComponent<FlatWorldGenerator>();
|
||||||
|
Debug.Log("[BlockSetup] Created FlatWorldGenerator.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── BlockPlacer на Player ─────────────────────────────────────────────
|
||||||
|
var player = GameObject.FindWithTag("Player");
|
||||||
|
if (player != null)
|
||||||
|
{
|
||||||
|
if (player.GetComponent<BlockPlacer>() == null)
|
||||||
|
{
|
||||||
|
var placer = player.AddComponent<BlockPlacer>();
|
||||||
|
|
||||||
|
// Назначаем breakMaterial
|
||||||
|
var breakShader = Shader.Find("Blocks/BlockBreak");
|
||||||
|
if (breakShader != null)
|
||||||
|
placer.breakMaterial = new Material(breakShader);
|
||||||
|
|
||||||
|
// Layer mask: ищем слой "BlockLayer", иначе напоминаем
|
||||||
|
int layer = LayerMask.NameToLayer("BlockLayer");
|
||||||
|
if (layer >= 0)
|
||||||
|
placer.blockLayer = 1 << layer;
|
||||||
|
else
|
||||||
|
Debug.LogWarning("[BlockSetup] Layer 'BlockLayer' not found. Создай его и назначь чанкам, затем задай blockLayer в BlockPlacer.");
|
||||||
|
|
||||||
|
Debug.Log("[BlockSetup] Added BlockPlacer to Player.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[BlockSetup] Player not found. Добавь BlockPlacer на объект с тегом 'Player' вручную.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Selection.activeGameObject = wm.gameObject;
|
||||||
|
|
||||||
|
EditorUtility.DisplayDialog(
|
||||||
|
"Block World Setup",
|
||||||
|
"Готово!\n\n" +
|
||||||
|
"Что нужно сделать вручную:\n" +
|
||||||
|
"1. BlockRegistry → добавь свои BlockDefinition в список\n" +
|
||||||
|
"2. Создай слой 'BlockLayer' (Project Settings > Tags & Layers)\n" +
|
||||||
|
"3. Назначь слой 'BlockLayer' чанкам (через chunkPrefab или скрипт)\n" +
|
||||||
|
"4. BlockPlacer → назначь breakLayer и crackTexture\n" +
|
||||||
|
"5. Запусти сцену — FlatWorldGenerator сгенерирует мир автоматически",
|
||||||
|
"OK");
|
||||||
|
}
|
||||||
|
|
||||||
|
[MenuItem("Tools/Create Block Definition")]
|
||||||
|
static void CreateBlockDefinition()
|
||||||
|
{
|
||||||
|
string folder = "Assets/Blocks/Definitions";
|
||||||
|
if (!System.IO.Directory.Exists(folder))
|
||||||
|
System.IO.Directory.CreateDirectory(folder);
|
||||||
|
|
||||||
|
var def = ScriptableObject.CreateInstance<BlockDefinition>();
|
||||||
|
def.blockId = "new_block";
|
||||||
|
def.displayName = "New Block";
|
||||||
|
|
||||||
|
string path = AssetDatabase.GenerateUniqueAssetPath($"{folder}/NewBlock.asset");
|
||||||
|
AssetDatabase.CreateAsset(def, path);
|
||||||
|
AssetDatabase.SaveAssets();
|
||||||
|
Selection.activeObject = def;
|
||||||
|
Debug.Log($"[BlockSetup] Created BlockDefinition: {path}");
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/Blocks/Scripts/Editor/BlockSetupTool.cs.meta
Normal file
2
Assets/Blocks/Scripts/Editor/BlockSetupTool.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 8dfaacf2cb701dd42965a828524a39f5
|
||||||
65
Assets/Blocks/Scripts/FlatWorldGenerator.cs
Normal file
65
Assets/Blocks/Scripts/FlatWorldGenerator.cs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Генерирует плоский мир — слои блоков по всей сетке чанков.
|
||||||
|
/// Реализует простой интерфейс, который легко заменить на terrain-генератор.
|
||||||
|
/// </summary>
|
||||||
|
public class FlatWorldGenerator : MonoBehaviour
|
||||||
|
{
|
||||||
|
[Header("World Size (in chunks)")]
|
||||||
|
public int chunksX = 4;
|
||||||
|
public int chunksZ = 4;
|
||||||
|
|
||||||
|
[Header("Layers (bottom → top)")]
|
||||||
|
public LayerDef[] layers = new[]
|
||||||
|
{
|
||||||
|
new LayerDef { blockId = "stone", height = 3 },
|
||||||
|
new LayerDef { blockId = "dirt", height = 3 },
|
||||||
|
new LayerDef { blockId = "grass", height = 1 },
|
||||||
|
};
|
||||||
|
|
||||||
|
void Start()
|
||||||
|
{
|
||||||
|
if (WorldManager.Instance == null)
|
||||||
|
{
|
||||||
|
Debug.LogError("[FlatWorldGenerator] WorldManager not found.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Generate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Generate()
|
||||||
|
{
|
||||||
|
for (int cx = 0; cx < chunksX; cx++)
|
||||||
|
for (int cz = 0; cz < chunksZ; cz++)
|
||||||
|
{
|
||||||
|
// Центрируем мир относительно нуля
|
||||||
|
var chunkPos = new Vector3Int(cx - chunksX / 2, 0, cz - chunksZ / 2);
|
||||||
|
var data = new ChunkData(chunkPos);
|
||||||
|
|
||||||
|
int currentY = 0;
|
||||||
|
foreach (var layer in layers)
|
||||||
|
{
|
||||||
|
for (int h = 0; h < layer.height; h++)
|
||||||
|
{
|
||||||
|
if (currentY >= ChunkData.HEIGHT) break;
|
||||||
|
for (int x = 0; x < ChunkData.SIZE; x++)
|
||||||
|
for (int z = 0; z < ChunkData.SIZE; z++)
|
||||||
|
data.SetBlock(x, currentY, z, layer.blockId);
|
||||||
|
currentY++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WorldManager.Instance.CreateChunk(chunkPos, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.Log($"[FlatWorldGenerator] Generated {chunksX * chunksZ} chunks.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[System.Serializable]
|
||||||
|
public struct LayerDef
|
||||||
|
{
|
||||||
|
public string blockId;
|
||||||
|
[Min(1)] public int height;
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/Blocks/Scripts/FlatWorldGenerator.cs.meta
Normal file
2
Assets/Blocks/Scripts/FlatWorldGenerator.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5f99c115771814a47a3582845ea6426b
|
||||||
140
Assets/Blocks/Scripts/WorldManager.cs
Normal file
140
Assets/Blocks/Scripts/WorldManager.cs
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Синглтон. Управляет всеми чанками.
|
||||||
|
/// Точка входа для постановки/разрушения блоков.
|
||||||
|
/// </summary>
|
||||||
|
public class WorldManager : MonoBehaviour
|
||||||
|
{
|
||||||
|
public static WorldManager Instance { get; private set; }
|
||||||
|
|
||||||
|
[Header("References")]
|
||||||
|
public BlockRegistry registry; // назначается автоматически если null
|
||||||
|
|
||||||
|
[Header("Chunk Prefab")]
|
||||||
|
[Tooltip("Можно оставить пустым — создаётся из кода")]
|
||||||
|
public GameObject chunkPrefab;
|
||||||
|
|
||||||
|
// chunkPos (в чанковых координатах) → Chunk
|
||||||
|
private readonly Dictionary<Vector3Int, Chunk> _chunks = new();
|
||||||
|
|
||||||
|
void Awake()
|
||||||
|
{
|
||||||
|
if (Instance != null) { Destroy(gameObject); return; }
|
||||||
|
Instance = this;
|
||||||
|
DontDestroyOnLoad(gameObject);
|
||||||
|
|
||||||
|
if (registry == null)
|
||||||
|
registry = FindObjectOfType<BlockRegistry>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Публичный API ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>Получить blockId по мировым координатам блока. null = пусто.</summary>
|
||||||
|
public string GetBlock(Vector3Int worldPos)
|
||||||
|
{
|
||||||
|
Vector3Int cp = ChunkData.WorldToChunkPos(worldPos);
|
||||||
|
if (!_chunks.TryGetValue(cp, out var chunk)) return null;
|
||||||
|
return chunk.GetBlock(ChunkData.WorldToLocal(worldPos));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Поставить блок. Создаёт чанк если нужно.</summary>
|
||||||
|
public void PlaceBlock(Vector3Int worldPos, string blockId)
|
||||||
|
{
|
||||||
|
var chunk = GetOrCreateChunk(ChunkData.WorldToChunkPos(worldPos));
|
||||||
|
chunk.SetBlock(ChunkData.WorldToLocal(worldPos), blockId);
|
||||||
|
|
||||||
|
// Соседние чанки на границе тоже надо пересобрать (face culling)
|
||||||
|
RebuildNeighboursIfBorder(worldPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Сломать блок. Возвращает blockId удалённого блока (или null).</summary>
|
||||||
|
public string BreakBlock(Vector3Int worldPos)
|
||||||
|
{
|
||||||
|
Vector3Int cp = ChunkData.WorldToChunkPos(worldPos);
|
||||||
|
if (!_chunks.TryGetValue(cp, out var chunk)) return null;
|
||||||
|
|
||||||
|
string blockId = chunk.GetBlock(ChunkData.WorldToLocal(worldPos));
|
||||||
|
if (blockId == null) return null;
|
||||||
|
|
||||||
|
chunk.RemoveBlock(ChunkData.WorldToLocal(worldPos));
|
||||||
|
RebuildNeighboursIfBorder(worldPos);
|
||||||
|
return blockId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получить или создать чанк по чанковым координатам.
|
||||||
|
/// Используется генераторами мира и PlaceBlock.
|
||||||
|
/// </summary>
|
||||||
|
public Chunk GetOrCreateChunk(Vector3Int chunkPos)
|
||||||
|
{
|
||||||
|
if (_chunks.TryGetValue(chunkPos, out var existing))
|
||||||
|
return existing;
|
||||||
|
|
||||||
|
return CreateChunk(chunkPos, new ChunkData(chunkPos));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Создать чанк с готовыми данными (используется генераторами мира).
|
||||||
|
/// </summary>
|
||||||
|
public Chunk CreateChunk(Vector3Int chunkPos, ChunkData data)
|
||||||
|
{
|
||||||
|
if (_chunks.ContainsKey(chunkPos))
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"[WorldManager] Chunk {chunkPos} already exists.");
|
||||||
|
return _chunks[chunkPos];
|
||||||
|
}
|
||||||
|
|
||||||
|
GameObject go;
|
||||||
|
if (chunkPrefab != null)
|
||||||
|
go = Instantiate(chunkPrefab, transform);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
go = new GameObject($"Chunk_{chunkPos}");
|
||||||
|
go.transform.SetParent(transform);
|
||||||
|
}
|
||||||
|
|
||||||
|
go.transform.position = ChunkData.ChunkToWorld(chunkPos);
|
||||||
|
|
||||||
|
var chunk = go.GetComponent<Chunk>() ?? go.AddComponent<Chunk>();
|
||||||
|
chunk.Initialise(data, registry.ChunkMaterial);
|
||||||
|
|
||||||
|
_chunks[chunkPos] = chunk;
|
||||||
|
return chunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Вспомогательные ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Если изменённый блок находится на границе чанка —
|
||||||
|
/// пересобираем соседний чанк, чтобы обновить face culling.
|
||||||
|
/// </summary>
|
||||||
|
void RebuildNeighboursIfBorder(Vector3Int worldPos)
|
||||||
|
{
|
||||||
|
Vector3Int local = ChunkData.WorldToLocal(worldPos);
|
||||||
|
|
||||||
|
if (local.x == 0) RebuildChunkAt(worldPos + Vector3Int.left);
|
||||||
|
if (local.x == ChunkData.SIZE - 1) RebuildChunkAt(worldPos + Vector3Int.right);
|
||||||
|
if (local.y == 0) RebuildChunkAt(worldPos + Vector3Int.down);
|
||||||
|
if (local.y == ChunkData.HEIGHT - 1) RebuildChunkAt(worldPos + Vector3Int.up);
|
||||||
|
if (local.z == 0) RebuildChunkAt(worldPos + new Vector3Int(0,0,-1));
|
||||||
|
if (local.z == ChunkData.SIZE - 1) RebuildChunkAt(worldPos + new Vector3Int(0,0,1));
|
||||||
|
}
|
||||||
|
|
||||||
|
void RebuildChunkAt(Vector3Int worldPos)
|
||||||
|
{
|
||||||
|
Vector3Int cp = ChunkData.WorldToChunkPos(worldPos);
|
||||||
|
if (_chunks.TryGetValue(cp, out var chunk))
|
||||||
|
chunk.RebuildMesh();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Перевод мировой позиции игрока → мировые координаты блока.</summary>
|
||||||
|
public static Vector3Int WorldToBlock(Vector3 worldPosition)
|
||||||
|
{
|
||||||
|
return new Vector3Int(
|
||||||
|
Mathf.FloorToInt(worldPosition.x),
|
||||||
|
Mathf.FloorToInt(worldPosition.y),
|
||||||
|
Mathf.FloorToInt(worldPosition.z));
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/Blocks/Scripts/WorldManager.cs.meta
Normal file
2
Assets/Blocks/Scripts/WorldManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5943e32593767dd4aa42fd361ea0be34
|
||||||
8
Assets/Blocks/Shaders.meta
Normal file
8
Assets/Blocks/Shaders.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 21faed0573fd7754eb9c39e314f180a7
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
58
Assets/Blocks/Shaders/BlockBreak.shader
Normal file
58
Assets/Blocks/Shaders/BlockBreak.shader
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
Shader "Blocks/BlockBreak"
|
||||||
|
{
|
||||||
|
// Оверлей анимации разрушения блока.
|
||||||
|
// Рендерится поверх чанкового меша отдельным объектом 1x1x1.
|
||||||
|
Properties
|
||||||
|
{
|
||||||
|
_Progress ("Break Progress", Range(0,1)) = 0
|
||||||
|
_CrackTex ("Crack Texture", 2D) = "black" {}
|
||||||
|
_Color ("Tint", Color) = (0,0,0,1)
|
||||||
|
}
|
||||||
|
|
||||||
|
SubShader
|
||||||
|
{
|
||||||
|
Tags
|
||||||
|
{
|
||||||
|
"Queue" = "Transparent+1"
|
||||||
|
"RenderType" = "Transparent"
|
||||||
|
"IgnoreProjector" = "True"
|
||||||
|
}
|
||||||
|
|
||||||
|
ZWrite Off
|
||||||
|
Blend SrcAlpha OneMinusSrcAlpha
|
||||||
|
Offset -1, -1 // рисуем чуть поверх, без z-fighting
|
||||||
|
|
||||||
|
Pass
|
||||||
|
{
|
||||||
|
CGPROGRAM
|
||||||
|
#pragma vertex vert
|
||||||
|
#pragma fragment frag
|
||||||
|
#include "UnityCG.cginc"
|
||||||
|
|
||||||
|
sampler2D _CrackTex;
|
||||||
|
float4 _CrackTex_ST;
|
||||||
|
float _Progress;
|
||||||
|
fixed4 _Color;
|
||||||
|
|
||||||
|
struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; };
|
||||||
|
struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; };
|
||||||
|
|
||||||
|
v2f vert(appdata v)
|
||||||
|
{
|
||||||
|
v2f o;
|
||||||
|
o.pos = UnityObjectToClipPos(v.vertex);
|
||||||
|
o.uv = TRANSFORM_TEX(v.uv, _CrackTex);
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
|
||||||
|
fixed4 frag(v2f i) : SV_Target
|
||||||
|
{
|
||||||
|
fixed4 crack = tex2D(_CrackTex, i.uv);
|
||||||
|
// crack.r — интенсивность трещины; накладывается по прогрессу
|
||||||
|
float alpha = crack.r * _Progress;
|
||||||
|
return fixed4(_Color.rgb, alpha);
|
||||||
|
}
|
||||||
|
ENDCG
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
Assets/Blocks/Shaders/BlockBreak.shader.meta
Normal file
9
Assets/Blocks/Shaders/BlockBreak.shader.meta
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: cb42248eaf3ad004dab200504cb05d47
|
||||||
|
ShaderImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
defaultTextures: []
|
||||||
|
nonModifiableTextures: []
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
55
Assets/Blocks/Shaders/VoxelLit.shader
Normal file
55
Assets/Blocks/Shaders/VoxelLit.shader
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
Shader "Blocks/VoxelLit"
|
||||||
|
{
|
||||||
|
Properties
|
||||||
|
{
|
||||||
|
_MainTexArray ("Texture Array", 2DArray) = "" {}
|
||||||
|
_Glossiness ("Smoothness", Range(0,1)) = 0.0
|
||||||
|
_Metallic ("Metallic", Range(0,1)) = 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
SubShader
|
||||||
|
{
|
||||||
|
Tags { "RenderType"="Opaque" }
|
||||||
|
LOD 200
|
||||||
|
|
||||||
|
CGPROGRAM
|
||||||
|
// Standard surface shader с кастомным vertex для передачи texIndex
|
||||||
|
#pragma surface surf Standard fullforwardshadows vertex:vert
|
||||||
|
#pragma target 3.5
|
||||||
|
#pragma require 2darray
|
||||||
|
|
||||||
|
UNITY_DECLARE_TEX2DARRAY(_MainTexArray);
|
||||||
|
|
||||||
|
struct Input
|
||||||
|
{
|
||||||
|
float2 uv_MainTexArray; // стандартный UV (0-1 по грани)
|
||||||
|
float texIndex; // индекс слоя в Texture2DArray
|
||||||
|
};
|
||||||
|
|
||||||
|
half _Glossiness;
|
||||||
|
half _Metallic;
|
||||||
|
|
||||||
|
// Vertex-функция: читаем texIndex из канала UV1 (mesh.uv2)
|
||||||
|
void vert(inout appdata_full v, out Input o)
|
||||||
|
{
|
||||||
|
UNITY_INITIALIZE_OUTPUT(Input, o);
|
||||||
|
o.uv_MainTexArray = v.texcoord.xy;
|
||||||
|
o.texIndex = v.texcoord1.x; // UV1.x = индекс текстуры
|
||||||
|
}
|
||||||
|
|
||||||
|
void surf(Input IN, inout SurfaceOutputStandard o)
|
||||||
|
{
|
||||||
|
fixed4 c = UNITY_SAMPLE_TEX2DARRAY(
|
||||||
|
_MainTexArray,
|
||||||
|
float3(IN.uv_MainTexArray, IN.texIndex)
|
||||||
|
);
|
||||||
|
o.Albedo = c.rgb;
|
||||||
|
o.Metallic = _Metallic;
|
||||||
|
o.Smoothness = _Glossiness;
|
||||||
|
o.Alpha = c.a;
|
||||||
|
}
|
||||||
|
ENDCG
|
||||||
|
}
|
||||||
|
|
||||||
|
FallBack "Diffuse"
|
||||||
|
}
|
||||||
9
Assets/Blocks/Shaders/VoxelLit.shader.meta
Normal file
9
Assets/Blocks/Shaders/VoxelLit.shader.meta
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 0061d561024e79d49851acdeda5f39c9
|
||||||
|
ShaderImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
defaultTextures: []
|
||||||
|
nonModifiableTextures: []
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -10,14 +10,14 @@ MonoBehaviour:
|
|||||||
m_Enabled: 1
|
m_Enabled: 1
|
||||||
m_EditorHideFlags: 0
|
m_EditorHideFlags: 0
|
||||||
m_Script: {fileID: 11500000, guid: 5741b63764436354ebad192c9b71f06c, type: 3}
|
m_Script: {fileID: 11500000, guid: 5741b63764436354ebad192c9b71f06c, type: 3}
|
||||||
m_Name: NewItem
|
m_Name: newItem
|
||||||
m_EditorClassIdentifier: Assembly-CSharp::ItemData
|
m_EditorClassIdentifier: Assembly-CSharp::ItemData
|
||||||
id: d28a3db9-17bd-489d-9caf-739005a7c780
|
id: c58a6ea4-f064-4392-becd-3b4c2b17cd06
|
||||||
itemName: NewItem
|
itemName: newItem
|
||||||
description: 123
|
description: 123123
|
||||||
type: 5
|
type: 5
|
||||||
modelPrefab: {fileID: 282714382156279063, guid: 4fcbba5b8725e254f98c052c4db89e02, type: 3}
|
modelPrefab: {fileID: 282714382156279063, guid: 4fcbba5b8725e254f98c052c4db89e02, type: 3}
|
||||||
icon: {fileID: 21300000, guid: 0115b2d990086ae4cbc31baf7c64e680, type: 3}
|
icon: {fileID: 0}
|
||||||
maxStack: 64
|
maxStack: 64
|
||||||
isCraftable: 0
|
isCraftable: 0
|
||||||
craftingRecipe:
|
craftingRecipe:
|
||||||
|
|||||||
31
Assets/Inventory/Items/ferstitem.asset
Normal file
31
Assets/Inventory/Items/ferstitem.asset
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!114 &11400000
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 0}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: 5741b63764436354ebad192c9b71f06c, type: 3}
|
||||||
|
m_Name: ferstitem
|
||||||
|
m_EditorClassIdentifier: Assembly-CSharp::ItemData
|
||||||
|
id: c3b1d462-dc84-4d7a-8463-34ffbde783d9
|
||||||
|
itemName: ferstitem
|
||||||
|
description: ferstitem
|
||||||
|
type: 5
|
||||||
|
modelPrefab: {fileID: 282714382156279063, guid: 4fcbba5b8725e254f98c052c4db89e02, type: 3}
|
||||||
|
icon: {fileID: 21300000, guid: 0115b2d990086ae4cbc31baf7c64e680, type: 3}
|
||||||
|
maxStack: 64
|
||||||
|
isCraftable: 0
|
||||||
|
craftingRecipe:
|
||||||
|
ingredients: []
|
||||||
|
amounts:
|
||||||
|
resultId:
|
||||||
|
resultAmount: 1
|
||||||
|
weight: 1
|
||||||
|
value: 1
|
||||||
|
isDroppable: 1
|
||||||
|
isStackable: 1
|
||||||
8
Assets/Inventory/Items/ferstitem.asset.meta
Normal file
8
Assets/Inventory/Items/ferstitem.asset.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d870f627519521f4aa24b9a36766465d
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
31
Assets/Inventory/Items/ferstitem123.asset
Normal file
31
Assets/Inventory/Items/ferstitem123.asset
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!114 &11400000
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 0}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: 5741b63764436354ebad192c9b71f06c, type: 3}
|
||||||
|
m_Name: ferstitem123
|
||||||
|
m_EditorClassIdentifier: Assembly-CSharp::ItemData
|
||||||
|
id: 4b147ba6-462a-4f90-9d00-29089fed390a
|
||||||
|
itemName: ferstitem123
|
||||||
|
description: 123
|
||||||
|
type: 5
|
||||||
|
modelPrefab: {fileID: 8569222662204961130, guid: 06e51a4df39df524397e91edba2a833d, type: 3}
|
||||||
|
icon: {fileID: 21300000, guid: 0115b2d990086ae4cbc31baf7c64e680, type: 3}
|
||||||
|
maxStack: 64
|
||||||
|
isCraftable: 0
|
||||||
|
craftingRecipe:
|
||||||
|
ingredients: []
|
||||||
|
amounts:
|
||||||
|
resultId:
|
||||||
|
resultAmount: 1
|
||||||
|
weight: 1
|
||||||
|
value: 1
|
||||||
|
isDroppable: 1
|
||||||
|
isStackable: 1
|
||||||
8
Assets/Inventory/Items/ferstitem123.asset.meta
Normal file
8
Assets/Inventory/Items/ferstitem123.asset.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5db581fdeb06daf4da37cb3d6f32b5ff
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -56,8 +56,8 @@ public class InventoryUIBuilder : MonoBehaviour
|
|||||||
GameObject inventoryPanel = CreatePanel("InventoryPanel", transform, inventoryPanelColor);
|
GameObject inventoryPanel = CreatePanel("InventoryPanel", transform, inventoryPanelColor);
|
||||||
inventoryPanelRect = inventoryPanel.GetComponent<RectTransform>();
|
inventoryPanelRect = inventoryPanel.GetComponent<RectTransform>();
|
||||||
|
|
||||||
float inventoryWidth = inventoryCols * (slotSize + slotSpacing) + inventoryPadding * 2;
|
float inventoryWidth = inventoryCols * slotSize + (inventoryCols - 1) * slotSpacing + inventoryPadding * 2;
|
||||||
float inventoryHeight = inventoryRows * (slotSize + slotSpacing) + inventoryPadding * 2 + 40;
|
float inventoryHeight = inventoryRows * slotSize + (inventoryRows - 1) * slotSpacing + (inventoryPadding + 30) * 2;
|
||||||
|
|
||||||
inventoryPanelRect.anchorMin = new Vector2(0.5f, 0.5f);
|
inventoryPanelRect.anchorMin = new Vector2(0.5f, 0.5f);
|
||||||
inventoryPanelRect.anchorMax = new Vector2(0.5f, 0.5f);
|
inventoryPanelRect.anchorMax = new Vector2(0.5f, 0.5f);
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ public class ItemBuilderTool : EditorWindow
|
|||||||
item.description = description;
|
item.description = description;
|
||||||
item.type = itemType;
|
item.type = itemType;
|
||||||
item.modelPrefab = modelPrefab;
|
item.modelPrefab = modelPrefab;
|
||||||
item.icon = icon != null ? icon : CreatePlaceholderIcon();
|
item.icon = icon ?? CreatePlaceholderIcon();
|
||||||
item.maxStack = maxStack;
|
item.maxStack = maxStack;
|
||||||
item.value = value;
|
item.value = value;
|
||||||
item.weight = weight;
|
item.weight = weight;
|
||||||
@@ -155,6 +155,14 @@ public class ItemBuilderTool : EditorWindow
|
|||||||
var worldItem = itemGO.AddComponent<WorldItem>();
|
var worldItem = itemGO.AddComponent<WorldItem>();
|
||||||
worldItem.interactRange = interactRange;
|
worldItem.interactRange = interactRange;
|
||||||
|
|
||||||
|
// Автоматически создаём/находим ItemData и назначаем
|
||||||
|
var data = EnsureItemData();
|
||||||
|
if (data != null)
|
||||||
|
{
|
||||||
|
worldItem.itemData = data;
|
||||||
|
AddToInventoryDatabase(data);
|
||||||
|
}
|
||||||
|
|
||||||
// Trigger-коллайдер для OverlapSphere (не для авто-pickup!)
|
// Trigger-коллайдер для OverlapSphere (не для авто-pickup!)
|
||||||
var col = itemGO.AddComponent<SphereCollider>();
|
var col = itemGO.AddComponent<SphereCollider>();
|
||||||
col.radius = 0.5f;
|
col.radius = 0.5f;
|
||||||
@@ -196,6 +204,13 @@ public class ItemBuilderTool : EditorWindow
|
|||||||
var worldItem = prefabGO.AddComponent<WorldItem>();
|
var worldItem = prefabGO.AddComponent<WorldItem>();
|
||||||
worldItem.interactRange = interactRange;
|
worldItem.interactRange = interactRange;
|
||||||
|
|
||||||
|
var data = EnsureItemData();
|
||||||
|
if (data != null)
|
||||||
|
{
|
||||||
|
worldItem.itemData = data;
|
||||||
|
AddToInventoryDatabase(data);
|
||||||
|
}
|
||||||
|
|
||||||
var col = prefabGO.AddComponent<SphereCollider>();
|
var col = prefabGO.AddComponent<SphereCollider>();
|
||||||
col.radius = 0.5f;
|
col.radius = 0.5f;
|
||||||
col.isTrigger = true;
|
col.isTrigger = true;
|
||||||
@@ -207,6 +222,52 @@ public class ItemBuilderTool : EditorWindow
|
|||||||
Debug.Log($"[ItemBuilder] Префаб сохранён: {path}");
|
Debug.Log($"[ItemBuilder] Префаб сохранён: {path}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Находит существующий ItemData ассет или создаёт новый — без диалогов
|
||||||
|
ItemData EnsureItemData()
|
||||||
|
{
|
||||||
|
string folder = "Assets/Inventory/Items";
|
||||||
|
string path = $"{folder}/{itemName}.asset";
|
||||||
|
|
||||||
|
var existing = AssetDatabase.LoadAssetAtPath<ItemData>(path);
|
||||||
|
if (existing != null) return existing;
|
||||||
|
|
||||||
|
if (!System.IO.Directory.Exists(folder))
|
||||||
|
System.IO.Directory.CreateDirectory(folder);
|
||||||
|
|
||||||
|
var item = ScriptableObject.CreateInstance<ItemData>();
|
||||||
|
item.id = string.IsNullOrEmpty(itemId) ? System.Guid.NewGuid().ToString() : itemId;
|
||||||
|
item.itemName = itemName;
|
||||||
|
item.description = description;
|
||||||
|
item.type = itemType;
|
||||||
|
item.modelPrefab = modelPrefab;
|
||||||
|
item.icon = icon ?? CreatePlaceholderIcon();
|
||||||
|
item.maxStack = maxStack;
|
||||||
|
item.value = value;
|
||||||
|
item.weight = weight;
|
||||||
|
item.isDroppable = isDroppable;
|
||||||
|
item.isStackable = isStackable;
|
||||||
|
|
||||||
|
AssetDatabase.CreateAsset(item, path);
|
||||||
|
AssetDatabase.SaveAssets();
|
||||||
|
Debug.Log($"[ItemBuilder] ItemData автоматически создан: {path}");
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляет ItemData в InventoryManager.itemDatabase если его там нет
|
||||||
|
void AddToInventoryDatabase(ItemData data)
|
||||||
|
{
|
||||||
|
var im = Object.FindObjectOfType<InventoryManager>();
|
||||||
|
if (im == null) return;
|
||||||
|
if (im.itemDatabase == null)
|
||||||
|
im.itemDatabase = new System.Collections.Generic.List<ItemData>();
|
||||||
|
if (!im.itemDatabase.Contains(data))
|
||||||
|
{
|
||||||
|
im.itemDatabase.Add(data);
|
||||||
|
EditorUtility.SetDirty(im);
|
||||||
|
Debug.Log($"[ItemBuilder] {data.itemName} добавлен в InventoryManager.itemDatabase");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Sprite CreatePlaceholderIcon()
|
Sprite CreatePlaceholderIcon()
|
||||||
{
|
{
|
||||||
var tex = new Texture2D(64, 64);
|
var tex = new Texture2D(64, 64);
|
||||||
|
|||||||
@@ -219,20 +219,6 @@ public class Slot : MonoBehaviour
|
|||||||
|
|
||||||
// ==================== UI Events ====================
|
// ==================== UI Events ====================
|
||||||
|
|
||||||
void OnMouseEnter()
|
|
||||||
{
|
|
||||||
if (slotBackground != null && !_isSelected)
|
|
||||||
slotBackground.color = hoverColor;
|
|
||||||
|
|
||||||
OnSlotHovered?.Invoke(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
void OnMouseExit()
|
|
||||||
{
|
|
||||||
if (slotBackground != null && !_isSelected)
|
|
||||||
slotBackground.color = IsEmpty ? emptySlotColor : filledSlotColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
void OnMouseDown()
|
void OnMouseDown()
|
||||||
{
|
{
|
||||||
if (Input.GetMouseButton(0))
|
if (Input.GetMouseButton(0))
|
||||||
|
|||||||
@@ -14,9 +14,24 @@ public class WorldItem : MonoBehaviour
|
|||||||
public float interactRange = 2.5f;
|
public float interactRange = 2.5f;
|
||||||
|
|
||||||
private bool _canPickup;
|
private bool _canPickup;
|
||||||
|
private bool _setupCalled;
|
||||||
|
|
||||||
|
void Start()
|
||||||
|
{
|
||||||
|
// Объект размещён в сцене через редактор — Setup() не вызывается,
|
||||||
|
// поэтому инициализируем pickup здесь.
|
||||||
|
if (!_setupCalled)
|
||||||
|
{
|
||||||
|
if (pickupDelay > 0)
|
||||||
|
Invoke(nameof(EnablePickup), pickupDelay);
|
||||||
|
else
|
||||||
|
_canPickup = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void Setup(ItemData data, int count)
|
public void Setup(ItemData data, int count)
|
||||||
{
|
{
|
||||||
|
_setupCalled = true;
|
||||||
itemData = data;
|
itemData = data;
|
||||||
amount = count;
|
amount = count;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
%YAML 1.1
|
%YAML 1.1
|
||||||
%TAG !u! tag:unity3d.com,2011:
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
--- !u!1 &4546282257700047937
|
--- !u!1 &8569222662204961130
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
@@ -8,55 +8,55 @@ GameObject:
|
|||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
serializedVersion: 6
|
serializedVersion: 6
|
||||||
m_Component:
|
m_Component:
|
||||||
- component: {fileID: 1948853633481286021}
|
- component: {fileID: 3299916784270131221}
|
||||||
- component: {fileID: 6658874805278007762}
|
- component: {fileID: 5472821117555071674}
|
||||||
- component: {fileID: 3056228376157587138}
|
- component: {fileID: 1683734392604781716}
|
||||||
m_Layer: 0
|
m_Layer: 0
|
||||||
m_Name: NewItem
|
m_Name: ferstitem
|
||||||
m_TagString: Untagged
|
m_TagString: Untagged
|
||||||
m_Icon: {fileID: 0}
|
m_Icon: {fileID: 0}
|
||||||
m_NavMeshLayer: 0
|
m_NavMeshLayer: 0
|
||||||
m_StaticEditorFlags: 0
|
m_StaticEditorFlags: 0
|
||||||
m_IsActive: 1
|
m_IsActive: 1
|
||||||
--- !u!4 &1948853633481286021
|
--- !u!4 &3299916784270131221
|
||||||
Transform:
|
Transform:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
m_PrefabInstance: {fileID: 0}
|
m_PrefabInstance: {fileID: 0}
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
m_GameObject: {fileID: 4546282257700047937}
|
m_GameObject: {fileID: 8569222662204961130}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
m_ConstrainProportionsScale: 0
|
m_ConstrainProportionsScale: 0
|
||||||
m_Children:
|
m_Children:
|
||||||
- {fileID: 7434756968248499828}
|
- {fileID: 8775671899748787654}
|
||||||
m_Father: {fileID: 0}
|
m_Father: {fileID: 0}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
--- !u!114 &6658874805278007762
|
--- !u!114 &5472821117555071674
|
||||||
MonoBehaviour:
|
MonoBehaviour:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
m_PrefabInstance: {fileID: 0}
|
m_PrefabInstance: {fileID: 0}
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
m_GameObject: {fileID: 4546282257700047937}
|
m_GameObject: {fileID: 8569222662204961130}
|
||||||
m_Enabled: 1
|
m_Enabled: 1
|
||||||
m_EditorHideFlags: 0
|
m_EditorHideFlags: 0
|
||||||
m_Script: {fileID: 11500000, guid: b8ac54fbf9ed99e45a83319443952a91, type: 3}
|
m_Script: {fileID: 11500000, guid: b8ac54fbf9ed99e45a83319443952a91, type: 3}
|
||||||
m_Name:
|
m_Name:
|
||||||
m_EditorClassIdentifier: Assembly-CSharp::WorldItem
|
m_EditorClassIdentifier: Assembly-CSharp::WorldItem
|
||||||
itemData: {fileID: 0}
|
itemData: {fileID: 11400000, guid: d870f627519521f4aa24b9a36766465d, type: 2}
|
||||||
amount: 1
|
amount: 1
|
||||||
pickupDelay: 0
|
pickupDelay: 0
|
||||||
interactRange: 2.5
|
interactRange: 2.5
|
||||||
--- !u!135 &3056228376157587138
|
--- !u!135 &1683734392604781716
|
||||||
SphereCollider:
|
SphereCollider:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
m_PrefabInstance: {fileID: 0}
|
m_PrefabInstance: {fileID: 0}
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
m_GameObject: {fileID: 4546282257700047937}
|
m_GameObject: {fileID: 8569222662204961130}
|
||||||
m_Material: {fileID: 0}
|
m_Material: {fileID: 0}
|
||||||
m_IncludeLayers:
|
m_IncludeLayers:
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
@@ -71,13 +71,13 @@ SphereCollider:
|
|||||||
serializedVersion: 3
|
serializedVersion: 3
|
||||||
m_Radius: 0.5
|
m_Radius: 0.5
|
||||||
m_Center: {x: 0, y: 0, z: 0}
|
m_Center: {x: 0, y: 0, z: 0}
|
||||||
--- !u!1001 &1495056769490629797
|
--- !u!1001 &746297087722819351
|
||||||
PrefabInstance:
|
PrefabInstance:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_Modification:
|
m_Modification:
|
||||||
serializedVersion: 3
|
serializedVersion: 3
|
||||||
m_TransformParent: {fileID: 1948853633481286021}
|
m_TransformParent: {fileID: 3299916784270131221}
|
||||||
m_Modifications:
|
m_Modifications:
|
||||||
- target: {fileID: 282714382156279063, guid: 4fcbba5b8725e254f98c052c4db89e02, type: 3}
|
- target: {fileID: 282714382156279063, guid: 4fcbba5b8725e254f98c052c4db89e02, type: 3}
|
||||||
propertyPath: m_Name
|
propertyPath: m_Name
|
||||||
@@ -128,8 +128,8 @@ PrefabInstance:
|
|||||||
m_AddedGameObjects: []
|
m_AddedGameObjects: []
|
||||||
m_AddedComponents: []
|
m_AddedComponents: []
|
||||||
m_SourcePrefab: {fileID: 100100000, guid: 4fcbba5b8725e254f98c052c4db89e02, type: 3}
|
m_SourcePrefab: {fileID: 100100000, guid: 4fcbba5b8725e254f98c052c4db89e02, type: 3}
|
||||||
--- !u!4 &7434756968248499828 stripped
|
--- !u!4 &8775671899748787654 stripped
|
||||||
Transform:
|
Transform:
|
||||||
m_CorrespondingSourceObject: {fileID: 8327738622413892305, guid: 4fcbba5b8725e254f98c052c4db89e02, type: 3}
|
m_CorrespondingSourceObject: {fileID: 8327738622413892305, guid: 4fcbba5b8725e254f98c052c4db89e02, type: 3}
|
||||||
m_PrefabInstance: {fileID: 1495056769490629797}
|
m_PrefabInstance: {fileID: 746297087722819351}
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 9f444e9048146434087c3db3550617f9
|
guid: 06e51a4df39df524397e91edba2a833d
|
||||||
PrefabImporter:
|
PrefabImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
userData:
|
userData:
|
||||||
135
Assets/Items/ferstitem123.prefab
Normal file
135
Assets/Items/ferstitem123.prefab
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!1 &3773692352489507899
|
||||||
|
GameObject:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
serializedVersion: 6
|
||||||
|
m_Component:
|
||||||
|
- component: {fileID: 3475947316444918413}
|
||||||
|
- component: {fileID: 4358986541640902538}
|
||||||
|
- component: {fileID: 8290802686145622635}
|
||||||
|
m_Layer: 0
|
||||||
|
m_Name: ferstitem123
|
||||||
|
m_TagString: Untagged
|
||||||
|
m_Icon: {fileID: 0}
|
||||||
|
m_NavMeshLayer: 0
|
||||||
|
m_StaticEditorFlags: 0
|
||||||
|
m_IsActive: 1
|
||||||
|
--- !u!4 &3475947316444918413
|
||||||
|
Transform:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 3773692352489507899}
|
||||||
|
serializedVersion: 2
|
||||||
|
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||||
|
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||||
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
|
m_ConstrainProportionsScale: 0
|
||||||
|
m_Children:
|
||||||
|
- {fileID: 1739540476929525579}
|
||||||
|
m_Father: {fileID: 0}
|
||||||
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
|
--- !u!114 &4358986541640902538
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 3773692352489507899}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: b8ac54fbf9ed99e45a83319443952a91, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: Assembly-CSharp::WorldItem
|
||||||
|
itemData: {fileID: 11400000, guid: 5db581fdeb06daf4da37cb3d6f32b5ff, type: 2}
|
||||||
|
amount: 1
|
||||||
|
pickupDelay: 0
|
||||||
|
interactRange: 2.5
|
||||||
|
--- !u!135 &8290802686145622635
|
||||||
|
SphereCollider:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 3773692352489507899}
|
||||||
|
m_Material: {fileID: 0}
|
||||||
|
m_IncludeLayers:
|
||||||
|
serializedVersion: 2
|
||||||
|
m_Bits: 0
|
||||||
|
m_ExcludeLayers:
|
||||||
|
serializedVersion: 2
|
||||||
|
m_Bits: 0
|
||||||
|
m_LayerOverridePriority: 0
|
||||||
|
m_IsTrigger: 1
|
||||||
|
m_ProvidesContacts: 0
|
||||||
|
m_Enabled: 1
|
||||||
|
serializedVersion: 3
|
||||||
|
m_Radius: 0.5
|
||||||
|
m_Center: {x: 0, y: 0, z: 0}
|
||||||
|
--- !u!1001 &3886533962277390174
|
||||||
|
PrefabInstance:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
serializedVersion: 2
|
||||||
|
m_Modification:
|
||||||
|
serializedVersion: 3
|
||||||
|
m_TransformParent: {fileID: 3475947316444918413}
|
||||||
|
m_Modifications:
|
||||||
|
- target: {fileID: 3299916784270131221, guid: 06e51a4df39df524397e91edba2a833d, type: 3}
|
||||||
|
propertyPath: m_LocalPosition.x
|
||||||
|
value: 0
|
||||||
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 3299916784270131221, guid: 06e51a4df39df524397e91edba2a833d, type: 3}
|
||||||
|
propertyPath: m_LocalPosition.y
|
||||||
|
value: 0
|
||||||
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 3299916784270131221, guid: 06e51a4df39df524397e91edba2a833d, type: 3}
|
||||||
|
propertyPath: m_LocalPosition.z
|
||||||
|
value: 0
|
||||||
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 3299916784270131221, guid: 06e51a4df39df524397e91edba2a833d, type: 3}
|
||||||
|
propertyPath: m_LocalRotation.w
|
||||||
|
value: 1
|
||||||
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 3299916784270131221, guid: 06e51a4df39df524397e91edba2a833d, type: 3}
|
||||||
|
propertyPath: m_LocalRotation.x
|
||||||
|
value: 0
|
||||||
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 3299916784270131221, guid: 06e51a4df39df524397e91edba2a833d, type: 3}
|
||||||
|
propertyPath: m_LocalRotation.y
|
||||||
|
value: 0
|
||||||
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 3299916784270131221, guid: 06e51a4df39df524397e91edba2a833d, type: 3}
|
||||||
|
propertyPath: m_LocalRotation.z
|
||||||
|
value: 0
|
||||||
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 3299916784270131221, guid: 06e51a4df39df524397e91edba2a833d, type: 3}
|
||||||
|
propertyPath: m_LocalEulerAnglesHint.x
|
||||||
|
value: 0
|
||||||
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 3299916784270131221, guid: 06e51a4df39df524397e91edba2a833d, type: 3}
|
||||||
|
propertyPath: m_LocalEulerAnglesHint.y
|
||||||
|
value: 0
|
||||||
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 3299916784270131221, guid: 06e51a4df39df524397e91edba2a833d, type: 3}
|
||||||
|
propertyPath: m_LocalEulerAnglesHint.z
|
||||||
|
value: 0
|
||||||
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 8569222662204961130, guid: 06e51a4df39df524397e91edba2a833d, type: 3}
|
||||||
|
propertyPath: m_Name
|
||||||
|
value: Visual
|
||||||
|
objectReference: {fileID: 0}
|
||||||
|
m_RemovedComponents: []
|
||||||
|
m_RemovedGameObjects: []
|
||||||
|
m_AddedGameObjects: []
|
||||||
|
m_AddedComponents: []
|
||||||
|
m_SourcePrefab: {fileID: 100100000, guid: 06e51a4df39df524397e91edba2a833d, type: 3}
|
||||||
|
--- !u!4 &1739540476929525579 stripped
|
||||||
|
Transform:
|
||||||
|
m_CorrespondingSourceObject: {fileID: 3299916784270131221, guid: 06e51a4df39df524397e91edba2a833d, type: 3}
|
||||||
|
m_PrefabInstance: {fileID: 3886533962277390174}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
7
Assets/Items/ferstitem123.prefab.meta
Normal file
7
Assets/Items/ferstitem123.prefab.meta
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 0e4347f1177decb4a8671c25fe736183
|
||||||
|
PrefabImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
0
Assets/Items/newItem.prefab
Normal file
0
Assets/Items/newItem.prefab
Normal file
7
Assets/Items/newItem.prefab.meta
Normal file
7
Assets/Items/newItem.prefab.meta
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 580244ac82154544e87abf9ff24eaf82
|
||||||
|
PrefabImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
File diff suppressed because one or more lines are too long
34
CLAUDE.md
34
CLAUDE.md
@@ -26,8 +26,8 @@ Unity game project (no CLI build commands — use the Unity Editor). Two major s
|
|||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
### Input Layer — `PlayerInput` (singleton, DontDestroyOnLoad)
|
### Input Layer — `PlayerInput` (singleton, DontDestroyOnLoad)
|
||||||
All input is centralised in `Assets/charecter-controller/scripts/PlayerInput.cs`. It exposes:
|
All input is centralised in `Assets/charecter-controller/scripts/PlayerInput.cs`. It is backed by the **New Input System** (`Assets/Input/PlayerControls.inputactions`, action maps: `Gameplay` and `UI`) and supports runtime key rebinding persisted via PlayerPrefs. It exposes:
|
||||||
- Continuous properties: `MouseX`, `MouseY`, `Horizontal`, `Vertical`, `Crouch`, `Run`
|
- Continuous properties: `MoveInput`, `LookInput`, `Crouch`, `Run`
|
||||||
- C# events: `OnJumpPressed`, `OnPickupPressed`, `OnToggleInventory`, `OnDropPressed`, `OnHotbarSelect`
|
- C# events: `OnJumpPressed`, `OnPickupPressed`, `OnToggleInventory`, `OnDropPressed`, `OnHotbarSelect`
|
||||||
- `Enabled` flag — set to `false` when the inventory is open to block gameplay input
|
- `Enabled` flag — set to `false` when the inventory is open to block gameplay input
|
||||||
|
|
||||||
@@ -37,9 +37,14 @@ Both `charecter.cs` and `InventoryManager.cs` subscribe to `PlayerInput.Instance
|
|||||||
- Requires `CharacterController` component
|
- Requires `CharacterController` component
|
||||||
- Head-bone / body separation: body yaw tracks camera yaw when moving; head offset is clamped to `±maxHeadYaw`
|
- Head-bone / body separation: body yaw tracks camera yaw when moving; head offset is clamped to `±maxHeadYaw`
|
||||||
- Bone rotations applied in `LateUpdate` via `ApplyRotations()`
|
- Bone rotations applied in `LateUpdate` via `ApplyRotations()`
|
||||||
- Animator hashes cached as `static readonly int` fields; animator Speed param: 0 = idle, 0.5 = walk, 1 = run
|
- Animator hashes cached as `static readonly int` fields; animator Speed param: 0 = idle, 0.5 = walk, 1 = run; also drives `IsMoving`, `IsCrouching`, `IsGrounded`
|
||||||
- Item pickup radius-cast in `TryPickupNearbyItem()` (2.5 m sphere, closest `WorldItem` wins)
|
- Item pickup radius-cast in `TryPickupNearbyItem()` (2.5 m sphere, closest `WorldItem` wins)
|
||||||
|
|
||||||
|
### Hand Equip System — `HandEquipSystem.cs`
|
||||||
|
- Attaches to the player; requires a `handBone` transform (right-hand bone)
|
||||||
|
- Listens to `PlayerInput.OnHotbarSelect` and `InventoryEvents.OnSlotChanged` to keep the in-hand 3D model in sync
|
||||||
|
- Instantiates `ItemData.modelPrefab` as a child of `handBone`; destroys the previous model on slot change
|
||||||
|
|
||||||
### Inventory System
|
### Inventory System
|
||||||
**Singleton chain** (all DontDestroyOnLoad, all auto-created if missing):
|
**Singleton chain** (all DontDestroyOnLoad, all auto-created if missing):
|
||||||
```
|
```
|
||||||
@@ -53,20 +58,35 @@ PlayerInput → InventoryManager → InventoryItemFactory
|
|||||||
- `AddItem()` fill order: existing main stacks → empty main slots → existing hotbar stacks → empty hotbar slots
|
- `AddItem()` fill order: existing main stacks → empty main slots → existing hotbar stacks → empty hotbar slots
|
||||||
- Save/load: JSON to `Application.persistentDataPath/inventory_save.json`; auto-save on `OnApplicationQuit`
|
- Save/load: JSON to `Application.persistentDataPath/inventory_save.json`; auto-save on `OnApplicationQuit`
|
||||||
|
|
||||||
**ItemData** — ScriptableObject (`Assets > Create > Inventory > Item`). `id` is auto-assigned as GUID on first `OnEnable`. ItemType enum: `Block, Weapon, Armor, Consumable, Tool, Material`.
|
**InventoryEvents** (`Assets/Inventory/Scripts/InventoryEvents.cs`) — singleton event bus (DontDestroyOnLoad):
|
||||||
|
- `OnItemAdded<string, int>`, `OnItemRemoved<string, int>`, `OnItemCountChanged<string, int>`
|
||||||
|
- `OnSlotChanged<int, string>` (slotIndex, itemId) — fired whenever a slot's contents change
|
||||||
|
- `OnInventoryOpened`, `OnInventoryClosed`
|
||||||
|
|
||||||
**InventoryItemFactory** — creates/drops `WorldItem` GameObjects. Needs an `ItemFactoryConfig` ScriptableObject (`Assets > Create > Inventory > Item Factory Config`) assigned in the Inspector for prefab-based spawning; falls back to a primitive cube if no prefab is configured.
|
**ItemData** — ScriptableObject (`Assets > Create > Inventory > Item`). `id` is auto-assigned as GUID on first `OnEnable`. ItemType enum: `Block, Weapon, Armor, Consumable, Tool, Material`. Each `ItemData` can embed a `craftingRecipe` directly.
|
||||||
|
|
||||||
**CraftingManager** — recipe key is sorted comma-joined ingredient IDs (`"id1,id2,id3"`). Recipes can be added at runtime via `AddRecipe()`.
|
**InventoryItemFactory** — creates/drops `WorldItem` GameObjects. Needs an `ItemFactoryConfig` ScriptableObject (`Assets > Create > Inventory > Item Factory Config`) assigned in the Inspector for prefab-based spawning; falls back to a primitive cube if no prefab is configured. Config fields: `worldItemPrefab`, `dropForce`, `dropRandomness`, `despawnTime`.
|
||||||
|
|
||||||
|
**InventoryDragDrop** — implements `IBeginDragHandler / IDragHandler / IEndDragHandler`:
|
||||||
|
- Drag between slots performs a `SwapSlots()` via raycast
|
||||||
|
- Dropping outside the inventory area calls `InventoryItemFactory.DropItem()` to spawn a `WorldItem`
|
||||||
|
|
||||||
|
**CraftingManager** — recipe key is sorted comma-joined ingredient IDs (`"id1,id2,id3"`). Recipes can be added at runtime via `AddRecipe()`. UI: 9 input slots, 1 output slot, craft button.
|
||||||
|
|
||||||
**InventoryUIBuilder** — generates the entire inventory UI programmatically at runtime (Canvas, GridLayoutGroup, Slot components, drag-drop handlers). Assigns `InventoryManager.Instance.slots` and `.hotbarSlots` directly. Do not assign these arrays in the Inspector when using this builder.
|
**InventoryUIBuilder** — generates the entire inventory UI programmatically at runtime (Canvas, GridLayoutGroup, Slot components, drag-drop handlers). Assigns `InventoryManager.Instance.slots` and `.hotbarSlots` directly. Do not assign these arrays in the Inspector when using this builder.
|
||||||
|
|
||||||
**InventorySetup** — convenience MonoBehaviour; add to a "GameManager" object to auto-create all managers and UI on scene start. Has an Editor button in the Inspector. Skips creation if `InventoryManager` already exists.
|
**InventorySetup** — convenience MonoBehaviour; add to a "GameManager" object to auto-create all managers and UI on scene start. Has an Editor button in the Inspector. Skips creation if `InventoryManager` already exists.
|
||||||
|
|
||||||
|
### Editor Tools (Tools menu)
|
||||||
|
These tools set up the scene at edit time — prefer them over manual wiring:
|
||||||
|
- **Tools > Setup Complete Scene** (`InventorySceneSetup.cs`) — creates EventSystem, all manager singletons, Player, InventoryCanvas, and PlayerInput in one click
|
||||||
|
- **Tools > Player Builder** (`PlayerBuilderTool.cs`) — creates CharacterController, `charecter.cs`, Camera, PlayerInput, HandEquipSystem; auto-detects head/hand bones; option to save as a prefab
|
||||||
|
- **Tools > Item Builder** (`ItemBuilderTool.cs`) — creates `ItemData` ScriptableObjects, WorldItem prefabs, and optionally places them in the scene; auto-generates placeholder icons
|
||||||
|
|
||||||
### Scene Setup
|
### Scene Setup
|
||||||
`Assets/Scenes/SampleScene.unity` is the only scene. Typical hierarchy:
|
`Assets/Scenes/SampleScene.unity` is the only scene. Typical hierarchy:
|
||||||
- `GameManager` with `InventorySetup` (or manually placed `InventoryManager` + `InventoryUIBuilder`)
|
- `GameManager` with `InventorySetup` (or manually placed `InventoryManager` + `InventoryUIBuilder`)
|
||||||
- Player with `charecter.cs` + `CharacterController` + Animator + head bone reference
|
- Player with `charecter.cs` + `CharacterController` + Animator + head bone reference + `HandEquipSystem` (hand bone reference)
|
||||||
|
|
||||||
### Meta Files
|
### Meta Files
|
||||||
Unity requires `.meta` files for all assets — always commit them alongside their assets.
|
Unity requires `.meta` files for all assets — always commit them alongside their assets.
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
<Solution>
|
<Solution>
|
||||||
<Project Path="Assembly-CSharp.csproj" />
|
<Project Path="Assembly-CSharp.csproj" />
|
||||||
|
<Project Path="Assembly-CSharp-Editor.csproj" />
|
||||||
</Solution>
|
</Solution>
|
||||||
|
|||||||
Reference in New Issue
Block a user