bloak
This commit is contained in:
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:
|
||||
Reference in New Issue
Block a user