This commit is contained in:
unknown
2026-04-16 18:19:23 +05:00
parent 6c3f72385e
commit 087db550f1
43 changed files with 1676 additions and 58 deletions

View File

@@ -1,7 +1,8 @@
{ {
"permissions": { "permissions": {
"allow": [ "allow": [
"WebSearch" "WebSearch",
"WebFetch(domain:docs.unity3d.com)"
] ]
} }
} }

8
Assets/Blocks.meta Normal file
View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: aeacaeb9429d7f747bc5884f30e48a3d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1ee7db48bfc98434ab8525bf0b140c62
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View 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;
}

View File

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

View 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;
}
}

View File

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

View 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);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 68a1a0f0ab870014ba9a973042ee35b4

View 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;
}
}

View File

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

View 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);
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3810cfa528149d9438c4fa8efe5f6611

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 223008930405c524fa586a9b9018ef71
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View 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}");
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8dfaacf2cb701dd42965a828524a39f5

View 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;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5f99c115771814a47a3582845ea6426b

View 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));
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5943e32593767dd4aa42fd361ea0be34

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 21faed0573fd7754eb9c39e314f180a7
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View 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
}
}
}

View File

@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: cb42248eaf3ad004dab200504cb05d47
ShaderImporter:
externalObjects: {}
defaultTextures: []
nonModifiableTextures: []
userData:
assetBundleName:
assetBundleVariant:

View 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"
}

View File

@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 0061d561024e79d49851acdeda5f39c9
ShaderImporter:
externalObjects: {}
defaultTextures: []
nonModifiableTextures: []
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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:

View 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

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d870f627519521f4aa24b9a36766465d
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View 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

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5db581fdeb06daf4da37cb3d6f32b5ff
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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);

View File

@@ -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);

View File

@@ -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))

View File

@@ -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;

View File

@@ -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}

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2 fileFormatVersion: 2
guid: 9f444e9048146434087c3db3550617f9 guid: 06e51a4df39df524397e91edba2a833d
PrefabImporter: PrefabImporter:
externalObjects: {} externalObjects: {}
userData: userData:

View 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}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 0e4347f1177decb4a8671c25fe736183
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

View 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

View File

@@ -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.

View File

@@ -1,3 +1,4 @@
<Solution> <Solution>
<Project Path="Assembly-CSharp.csproj" /> <Project Path="Assembly-CSharp.csproj" />
<Project Path="Assembly-CSharp-Editor.csproj" />
</Solution> </Solution>