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

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