HitboxDisplay lots of broken stuff

This commit is contained in:
ThomasCharester committed 2026-11-09 02:51:36 +03:00
1 parent 9203f269db
commit f40a10912c
78 files changed
+12768 -309066

No files matched your search

@@ -12,6 +12,9 @@ namespace Code.OfficeGame.BattleMode.Actions
public NavMeshPath Path;
public Vector3[] Corners;
public int CurrentCornerIndex;
public bool IsRotateOnly;
public int CurrentFrame;
public override BattleActionData GetSnapshot()
{
Vector3[] cornersCopy = null;
@@ -11,27 +11,151 @@ namespace Code.OfficeGame.BattleMode.Actions
{
private const float StoppingDistance = 0.15f;
private const float RotateDistance = 1f;
private const float NavMeshSampleRadius = 1.5f;
private const float TurnSpeedDegPerSec = 720f; // Скорость вращения на месте (градусов/сек)
private int MoveStateHash => Animator.StringToHash("WalkBlend");
public override BattleActionType ActionType => BattleActionType.Move;
public override DeciderType DeciderType => DeciderType.Position;
public override BattleActionData MountAndGetFrames(EntityProperties entity, BattleActionDecisionResult result)
{
MovementBattleActionData data = new();
var positionDecisionResult = (PositionBattleActionDeciderResult)result;
Vector3 startPos = entity.KinematicCharacterMotor.TransientPosition;
Vector3 targetPos = positionDecisionResult.Position;
// 1. Проверяем дистанцию в плоскости XZ
Vector3 toTargetXZ = targetPos - startPos;
toTargetXZ.y = 0f;
float distanceXZ = toTargetXZ.magnitude;
// ЕСЛИ ВПЛОТНУЮ: отключаем перемещение, оставляем только поворот
if (distanceXZ <= StoppingDistance)
{
data.IsRotateOnly = true;
data.Corners = null;
float angle = Quaternion.Angle(entity.KinematicCharacterMotor.TransientRotation, positionDecisionResult.Rotation);
// Если персонаж уже смотрит в нужную сторону — 0 кадров
if (angle < 1f)
{
data.TotalFrames = 0;
}
else
{
float stepPerFrame = TurnSpeedDegPerSec * BattleTimeline.TickDelta;
data.TotalFrames = Mathf.Max(2, Mathf.CeilToInt(angle / stepPerFrame));
}
return data;
}
// 2. ДВИЖЕНИЕ: проецируем точки строго на полигоны NavMesh
bool hasStart = NavMesh.SamplePosition(startPos, out NavMeshHit startHit, NavMeshSampleRadius, NavMesh.AllAreas);
bool hasTarget = NavMesh.SamplePosition(targetPos, out NavMeshHit targetHit, NavMeshSampleRadius, NavMesh.AllAreas);
if (!hasStart || !hasTarget)
{
data.TotalFrames = 0;
return data;
}
Vector3 validStart = startHit.position;
Vector3 validTarget = targetHit.position;
// 3. Прямая видимость: если препятствий нет, идём по идеальной прямой без квантования полигонов NavMesh
if (!NavMesh.Raycast(validStart, validTarget, out _, NavMesh.AllAreas))
{
data.Corners = new[] { startPos, validTarget };
data.CurrentCornerIndex = 1;
data.TotalFrames = CalculatePathFrames(data.Corners, entity.MovementController);
return data;
}
// 4. Обход препятствий: строим путь и отсекаем частичные обрубки (PathPartial)
NavMeshPath path = new();
if (NavMesh.CalculatePath(validStart, validTarget, NavMesh.AllAreas, path)
&& path.status == NavMeshPathStatus.PathComplete
&& path.corners.Length > 1)
{
// Привязываем нулевую и финальную точки к фактическим координатам
path.corners[0] = startPos;
path.corners[^1] = validTarget;
data.Corners = path.corners;
data.CurrentCornerIndex = 1;
data.TotalFrames = CalculatePathFrames(data.Corners, entity.MovementController);
}
else
{
// Если NavMesh не смог построить полный путь (PathPartial/Invalid) — не даём ходить огрызками
data.TotalFrames = 0;
}
return data;
}
public override void Tick(
float tickDelta,
EntityProperties entity,
BattleActionDecisionResult battleActionDecisionResult,
BattleActionData battleActionData)
{
if(battleActionData is not MovementBattleActionData movementActionData
if (battleActionData is not MovementBattleActionData movementActionData
|| battleActionDecisionResult is not PositionBattleActionDeciderResult positionDecisionResult)
{
battleActionData.Finished = true;
return;
}
var entityMovementController = entity.MovementController;
var movementController = entity.MovementController;
// -------------------------------------------------------------
// РЕЖИМ 1: ТОЛЬКО ПОВОРОТ НА МЕСТЕ (КОГДА ПОЗИЦИЯ ВПЛОТНУЮ)
// -------------------------------------------------------------
if (movementActionData.IsRotateOnly)
{
movementActionData.CurrentFrame++;
float angle = Quaternion.Angle(movementController.Motor.TransientRotation, positionDecisionResult.Rotation);
int framesLeft = Mathf.Max(1, movementActionData.TotalFrames - movementActionData.CurrentFrame + 1);
float stepAngle = angle / framesLeft;
Quaternion nextRot = Quaternion.RotateTowards(
movementController.Motor.TransientRotation,
positionDecisionResult.Rotation,
stepAngle
);
entity.KinematicCharacterMotor.SetRotation(nextRot);
StopMovementAndFinish(movementController);
KinematicCharacterSystem.Simulate(
tickDelta,
KinematicCharacterSystem.CharacterMotors,
KinematicCharacterSystem.PhysicsMovers
);
if (movementActionData.CurrentFrame >= movementActionData.TotalFrames || angle <= 0.5f)
{
entity.KinematicCharacterMotor.SetRotation(positionDecisionResult.Rotation);
battleActionData.Finished = true;
}
return;
}
// -------------------------------------------------------------
// РЕЖИМ 2: ПЕРЕДВИЖЕНИЕ ПО ПУТИ
// -------------------------------------------------------------
if (movementActionData.Corners == null || movementActionData.CurrentCornerIndex >= movementActionData.Corners.Length)
{
StopMovementAndFinish(entityMovementController);
entity.KinematicCharacterMotor.SetRotation(positionDecisionResult.Rotation);
StopMovementAndFinish(movementController);
battleActionData.Finished = true;
return;
}
@@ -39,14 +163,13 @@ namespace Code.OfficeGame.BattleMode.Actions
if (!movementActionData.WasStarted)
{
movementActionData.WasStarted = true;
float transitionDurationSeconds = 10 * BattleTimeline.TickDelta;
entity.animator.CrossFadeInFixedTime(MoveStateHash, transitionDurationSeconds);
}
Vector3 currentPos = entity.KinematicCharacterMotor.TransientPosition;
// Пропускаем пройденные углы
while (movementActionData.CurrentCornerIndex < movementActionData.Corners.Length)
{
Vector3 toCorner = movementActionData.Corners[movementActionData.CurrentCornerIndex] - currentPos;
@@ -58,53 +181,50 @@ namespace Code.OfficeGame.BattleMode.Actions
break;
}
// Если все точки пройдены до шага симуляции
if (movementActionData.CurrentCornerIndex >= movementActionData.Corners.Length)
{
StopMovementAndFinish(entity.MovementController);
entity.KinematicCharacterMotor.SetRotation(positionDecisionResult.Rotation);
StopMovementAndFinish(movementController);
KinematicCharacterSystem.Simulate(
tickDelta,
KinematicCharacterSystem.CharacterMotors,
KinematicCharacterSystem.PhysicsMovers
);
battleActionData.Finished = true;
return;
}
Vector3 targetCorner = movementActionData.Corners[movementActionData.CurrentCornerIndex];
Vector3 moveDirection = targetCorner - currentPos;
moveDirection.y = 0f;
float distance = moveDirection.magnitude;
Vector3 moveNormal = distance > 0.0001f ? moveDirection / distance : Vector3.zero;
bool preLastPoint = movementActionData.CurrentCornerIndex >= movementActionData.Corners.Length - 1
&& distance < RotateDistance;
float maxStep = entityMovementController.MaxStableMoveSpeed * tickDelta;
float inputMagnitude =
(movementActionData.CurrentCornerIndex == movementActionData.Corners.Length - 1
&& distance < maxStep
&& maxStep > 0f)
? distance / maxStep
: 1f;
Quaternion lookRotation;
if(preLastPoint) lookRotation = positionDecisionResult.Rotation;
else lookRotation = moveNormal.sqrMagnitude > 0.0001f
? Quaternion.LookRotation(moveNormal, entityMovementController.Motor.CharacterUp)
: entityMovementController.Motor.TransientRotation;
bool isLastCorner = movementActionData.CurrentCornerIndex == movementActionData.Corners.Length - 1;
bool preLastPoint = isLastCorner && distance < RotateDistance;
float maxStep = movementController.MaxStableMoveSpeed * tickDelta;
float inputMagnitude = (isLastCorner && distance < maxStep && maxStep > 0f)
? distance / maxStep
: 1f;
Quaternion lookRotation = preLastPoint
? positionDecisionResult.Rotation
: (moveNormal.sqrMagnitude > 0.0001f
? Quaternion.LookRotation(moveNormal, movementController.Motor.CharacterUp)
: movementController.Motor.TransientRotation);
var aiInputs = new AICharacterInputs
{
MoveVector = moveNormal * inputMagnitude,
Look = lookRotation
};
entityMovementController.SetInputs(ref aiInputs);
movementController.SetInputs(ref aiInputs);
KinematicCharacterSystem.Simulate(
tickDelta,
@@ -112,15 +232,15 @@ namespace Code.OfficeGame.BattleMode.Actions
KinematicCharacterSystem.PhysicsMovers
);
Vector3 postMovePos = entityMovementController.Motor.TransientPosition;
// Проверка достижения конечной точки после физического тика
Vector3 postMovePos = movementController.Motor.TransientPosition;
Vector3 toFinal = movementActionData.Corners[^1] - postMovePos;
toFinal.y = 0f;
if (movementActionData.CurrentCornerIndex >= movementActionData.Corners.Length - 1
&& toFinal.sqrMagnitude <= StoppingDistance * StoppingDistance)
if (isLastCorner && toFinal.sqrMagnitude <= StoppingDistance * StoppingDistance)
{
entity.KinematicCharacterMotor.SetRotation(positionDecisionResult.Rotation);
StopMovementAndFinish(entityMovementController);
StopMovementAndFinish(movementController);
battleActionData.Finished = true;
}
}
@@ -136,32 +256,6 @@ namespace Code.OfficeGame.BattleMode.Actions
movementController.Motor.BaseVelocity = Vector3.zero;
}
public override BattleActionType ActionType => BattleActionType.Move;
public override DeciderType DeciderType => DeciderType.Position;
// TODO всегда выдаёт разное количество кадров
public override BattleActionData MountAndGetFrames(EntityProperties entity, BattleActionDecisionResult result)
{
MovementBattleActionData movementBattleActionData = new();
Vector3 startPos = entity.KinematicCharacterMotor.TransientPosition;
NavMeshPath path = new();
var positionDecisionResult = (PositionBattleActionDeciderResult) result;
if ((positionDecisionResult.Position - startPos).sqrMagnitude > StoppingDistance * StoppingDistance
&& NavMesh.CalculatePath(startPos, positionDecisionResult.Position, NavMesh.AllAreas, path)
&& path.corners.Length > 0)
{
movementBattleActionData.Corners = path.corners;
movementBattleActionData.CurrentCornerIndex = movementBattleActionData.Corners.Length > 1 ? 1 : 0;
movementBattleActionData.TotalFrames = CalculatePathFrames(movementBattleActionData.Corners, entity.MovementController);
}
else movementBattleActionData.TotalFrames = 0;
return movementBattleActionData;
}
private int CalculatePathFrames(Vector3[] corners, MovementController movementController)
{
if (corners == null || corners.Length < 2)
@@ -174,17 +268,15 @@ namespace Code.OfficeGame.BattleMode.Actions
Vector3 segment = corners[i] - corners[i - 1];
segment.y = 0f;
totalDistanceXZ += segment.magnitude;
Debug.Log($"{i} corner is {segment.magnitude}");
}
float effectiveDistance = Mathf.Max(0f, totalDistanceXZ - StoppingDistance);
float stepPerFrame = movementController.MaxStableMoveSpeed * BattleTimeline.TickDelta;
if (stepPerFrame <= 0.0001f)
return 0;
return Mathf.CeilToInt(effectiveDistance / stepPerFrame);
}
return Mathf.Max(1, Mathf.CeilToInt(effectiveDistance / stepPerFrame));
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6a7bbf0cc25b4dadb86824c49477ed47
timeCreated: 1793990021
@@ -0,0 +1,182 @@
using System.Collections.Generic;
using UnityEngine;
namespace Code.OfficeGame.BattleMode.Actions
{
public class EntityPreviewVisuals : MonoBehaviour
{
[Header("Материалы для MKToon")]
[Tooltip("Полупрозрачный материал для статичного слепка в точке снапшота")]
[SerializeField]
private Material snapshotGhostMaterial;
[Tooltip(
"Опционально: материал для самой движущейся модели во время превью (если null — меняется только Rim/Tint)")]
[SerializeField]
private Material activePreviewMaterial;
[Header("Настройки подсветки MKToon (если activePreviewMaterial не задан)")] [SerializeField]
private Color previewAlbedoTint = new Color(0.6f, 0.85f, 1f, 1f);
[SerializeField, ColorUsage(true, true)]
private Color previewRimColor = new Color(0f, 1.5f, 2f, 1f);
private SkinnedMeshRenderer[] _skinnedRenderers;
private MeshRenderer[] _meshRenderers;
// Кэш оригинальных материалов
private readonly Dictionary<Renderer, Material[]> _originalMaterials = new();
// Пул объектов для статичного слепка (чтобы не делать Instantiate и new Mesh каждый цикл превью)
private GameObject _snapshotGhostRoot;
private readonly List<Mesh> _bakedMeshes = new();
private readonly List<MeshFilter> _ghostMeshFilters = new();
private readonly List<MeshRenderer> _ghostMeshRenderers = new();
private MaterialPropertyBlock _propBlock;
private static readonly int AlbedoColorId = Shader.PropertyToID("_AlbedoColor");
private static readonly int RimColorId = Shader.PropertyToID("_RimColor");
private void Awake()
{
_propBlock = new MaterialPropertyBlock();
_skinnedRenderers = GetComponentsInChildren<SkinnedMeshRenderer>();
_meshRenderers = GetComponentsInChildren<MeshRenderer>();
// Сохраняем родные материалы MKToon
foreach (var sr in _skinnedRenderers)
_originalMaterials[sr] = sr.sharedMaterials;
foreach (var mr in _meshRenderers)
_originalMaterials[mr] = mr.sharedMaterials;
InitializeGhostPool();
}
private void InitializeGhostPool()
{
_snapshotGhostRoot = new GameObject($"{gameObject.name}_SnapshotGhost");
_snapshotGhostRoot.SetActive(false);
// Создаём легковесные меш-объекты под каждый SkinnedMeshRenderer персонажа
for (int i = 0; i < _skinnedRenderers.Length; i++)
{
var partGo = new GameObject($"GhostPart_{i}");
partGo.transform.SetParent(_snapshotGhostRoot.transform, false);
var mf = partGo.AddComponent<MeshFilter>();
var mr = partGo.AddComponent<MeshRenderer>();
// Отключаем тени у призрака для чистоты картинки и производительности
mr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
var bakedMesh = new Mesh { name = $"BakedGhostMesh_{i}" };
mf.sharedMesh = bakedMesh;
_bakedMeshes.Add(bakedMesh);
_ghostMeshFilters.Add(mf);
_ghostMeshRenderers.Add(mr);
}
}
/// <summary>
/// Вызывается перед стартом цикла PreviewBattleLoopAsync (в момент снятия снапшота)
/// </summary>
public void ShowPreviewState(bool spawnStaticSnapshotGhost = true)
{
// 1. Запекаем полупрозрачную статую в текущей точке снапшота
if (spawnStaticSnapshotGhost && snapshotGhostMaterial != null)
{
_snapshotGhostRoot.transform.SetPositionAndRotation(transform.position, transform.rotation);
for (int i = 0; i < _skinnedRenderers.Length; i++)
{
var sr = _skinnedRenderers[i];
sr.BakeMesh(_bakedMeshes[i]); // Мгновенный снимок позы скелета!
var ghostTransform = _ghostMeshFilters[i].transform;
ghostTransform.SetPositionAndRotation(sr.transform.position, sr.transform.rotation);
// ghostTransform.localScale = sr.transform.lossyScale;
// Назначаем прозрачный материал на все саб-меши
int subMeshCount = _bakedMeshes[i].subMeshCount;
Material[] ghostMats = new Material[Mathf.Max(1, subMeshCount)];
for (int m = 0; m < ghostMats.Length; m++)
ghostMats[m] = snapshotGhostMaterial;
_ghostMeshRenderers[i].sharedMaterials = ghostMats;
}
_snapshotGhostRoot.SetActive(true);
}
// 2. Меняем внешний вид самой модели, которая сейчас побежит в превью
ApplyPreviewLookToMainModel();
}
/// <summary>
/// Вызывается в блоке finally в PreviewBattleLoopAsync
/// </summary>
public void HidePreviewState()
{
if (_snapshotGhostRoot != null)
_snapshotGhostRoot.SetActive(false);
RestoreMainModelLook();
}
private void ApplyPreviewLookToMainModel()
{
foreach (var (renderer, origMats) in _originalMaterials)
{
if (renderer == null) continue;
// Способ 1: Если задан отдельный материал для превью — подменяем sharedMaterials
if (activePreviewMaterial != null)
{
Material[] previewMats = new Material[origMats.Length];
for (int i = 0; i < previewMats.Length; i++)
previewMats[i] = activePreviewMaterial;
renderer.sharedMaterials = previewMats;
}
// Способ 2: Иначе подкрашиваем родной MKToon через MaterialPropertyBlock (без аллокаций)
else
{
renderer.GetPropertyBlock(_propBlock);
_propBlock.SetColor(AlbedoColorId, previewAlbedoTint);
_propBlock.SetColor(RimColorId, previewRimColor);
renderer.SetPropertyBlock(_propBlock);
}
}
}
private void RestoreMainModelLook()
{
foreach (var (renderer, origMats) in _originalMaterials)
{
if (renderer == null) continue;
if (activePreviewMaterial != null)
{
renderer.sharedMaterials = origMats;
}
else
{
renderer.SetPropertyBlock(null); // Сбрасывает все переопределения PropertyBlock
}
}
}
private void OnDestroy()
{
foreach (var mesh in _bakedMeshes)
{
if (mesh != null) Destroy(mesh);
}
if (_snapshotGhostRoot != null)
Destroy(_snapshotGhostRoot);
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 97e5f578c2c24bb7b3bd55d2b3ade97a
timeCreated: 1793990043
@@ -13,5 +13,6 @@ namespace Code.OfficeGame.BattleMode.Actions
public MovementController MovementController;
public KinematicCharacterMotor KinematicCharacterMotor;
public EntityHitBoxes EntityHitBoxes;
public EntityPreviewVisuals EntityPreviewVisuals;
}
}
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using Code.OfficeGame.BattleMode.Timeline;
using UnityEngine;
namespace Code.OfficeGame.BattleMode.HitBoxes
@@ -26,6 +27,8 @@ namespace Code.OfficeGame.BattleMode.HitBoxes
BoxCollider box = bodyParts[hitBoxPartType];
Transform boxTransform = box.transform;
HitboxDrawer.DrawBox(box, Color.red, BattleTimeline.TickDelta);
Vector3 worldCenter = boxTransform.TransformPoint(box.center);
Vector3 worldHalfExtents = Vector3.Scale(box.size * 0.5f, boxTransform.lossyScale);
@@ -0,0 +1,178 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
namespace Code.OfficeGame.BattleMode.HitBoxes
{
public static class HitboxDrawer
{
private struct ActiveBox
{
public Matrix4x4 Matrix;
public Color Color;
public float ExpireTime;
}
private static readonly List<ActiveBox> _boxes = new(16);
private static Mesh _cubeMesh;
private static Material _material;
private static MaterialPropertyBlock _propertyBlock;
private static readonly int BaseColorPropId = Shader.PropertyToID("_BaseColor");
private static bool _initialized;
/// <summary>
/// Отрисовка хитбокса напрямую по BoxCollider
/// </summary>
public static void DrawBox(BoxCollider collider, Color color, float duration = 1.0f)
{
if (collider == null) return;
Transform t = collider.transform;
Vector3 worldCenter = t.TransformPoint(collider.center);
Vector3 worldSize = Vector3.Scale(collider.size, t.lossyScale);
DrawBox(worldCenter, worldSize, t.rotation, color, duration);
}
/// <summary>
/// Отрисовка хитбокса по параметрам позиции, габаритов и вращения
/// </summary>
public static void DrawBox(Vector3 center, Vector3 size, Quaternion rotation, Color color,
float duration = 1.0f)
{
EnsureInitialized();
if (_material == null) return;
_boxes.Add(new ActiveBox
{
Matrix = Matrix4x4.TRS(center, rotation, size),
Color = color,
ExpireTime = Time.unscaledTime + duration
});
}
/// <summary>
/// Принудительная очистка всех активных хитбоксов
/// </summary>
public static void Clear()
{
_boxes.Clear();
}
private static void EnsureInitialized()
{
if (_initialized) return;
_cubeMesh = GenerateUnitCubeMesh();
_propertyBlock = new MaterialPropertyBlock();
var shader = Shader.Find("Custom/HitboxVisualizer");
if (shader != null)
{
_material = new Material(shader) { hideFlags = HideFlags.HideAndDontSave };
}
else
{
Debug.LogError("[HitboxDrawer] Не найден шейдер 'Custom/HitboxVisualizer'!");
return;
}
RenderPipelineManager.beginContextRendering += OnRender;
Camera.onPreCull += OnPreCullLegacy;
_initialized = true;
}
private static void OnRender(ScriptableRenderContext ctx, List<Camera> cameras) => RenderBoxes();
private static void OnPreCullLegacy(Camera cam)
{
if (GraphicsSettings.currentRenderPipeline == null) RenderBoxes();
}
private static void RenderBoxes()
{
int count = _boxes.Count;
if (count == 0) return;
float now = Time.unscaledTime;
for (int i = count - 1; i >= 0; i--)
{
ActiveBox box = _boxes[i];
if (now >= box.ExpireTime)
{
// Быстрое удаление за O(1)
_boxes[i] = _boxes[_boxes.Count - 1];
_boxes.RemoveAt(_boxes.Count - 1);
continue;
}
_propertyBlock.SetColor(BaseColorPropId, box.Color);
Graphics.DrawMesh(
_cubeMesh,
box.Matrix,
_material,
0,
null,
0,
_propertyBlock,
castShadows: false,
receiveShadows: false,
useLightProbes: false
);
}
}
private static Mesh GenerateUnitCubeMesh()
{
Mesh mesh = new Mesh { name = "Hitbox_UnitCube" };
// 24 вершины с уникальными UV на каждой грани для точной границы
Vector3[] vertices = new Vector3[24]
{
// Передняя (+Z)
new(-0.5f, -0.5f, 0.5f), new(0.5f, -0.5f, 0.5f), new(0.5f, 0.5f, 0.5f), new(-0.5f, 0.5f, 0.5f),
// Задняя (-Z)
new(0.5f, -0.5f, -0.5f), new(-0.5f, -0.5f, -0.5f), new(-0.5f, 0.5f, -0.5f), new(0.5f, 0.5f, -0.5f),
// Правая (+X)
new(0.5f, -0.5f, 0.5f), new(0.5f, -0.5f, -0.5f), new(0.5f, 0.5f, -0.5f), new(0.5f, 0.5f, 0.5f),
// Левая (-X)
new(-0.5f, -0.5f, -0.5f), new(-0.5f, -0.5f, 0.5f), new(-0.5f, 0.5f, 0.5f), new(-0.5f, 0.5f, -0.5f),
// Верхняя (+Y)
new(-0.5f, 0.5f, 0.5f), new(0.5f, 0.5f, 0.5f), new(0.5f, 0.5f, -0.5f), new(-0.5f, 0.5f, -0.5f),
// Нижняя (-Y)
new(-0.5f, -0.5f, -0.5f), new(0.5f, -0.5f, -0.5f), new(0.5f, -0.5f, 0.5f), new(-0.5f, -0.5f, 0.5f)
};
Vector2[] uvs = new Vector2[24];
int[] triangles = new int[36];
for (int face = 0; face < 6; face++)
{
int v = face * 4;
uvs[v + 0] = new Vector2(0, 0);
uvs[v + 1] = new Vector2(1, 0);
uvs[v + 2] = new Vector2(1, 1);
uvs[v + 3] = new Vector2(0, 1);
int t = face * 6;
triangles[t + 0] = v + 0;
triangles[t + 1] = v + 2;
triangles[t + 2] = v + 1;
triangles[t + 3] = v + 0;
triangles[t + 4] = v + 3;
triangles[t + 5] = v + 2;
}
mesh.vertices = vertices;
mesh.uv = uvs;
mesh.triangles = triangles;
mesh.RecalculateBounds();
return mesh;
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 9c609b6a872240fca020c17b9a66723c
timeCreated: 1794178985
@@ -0,0 +1,89 @@
Shader "Custom/HitboxVisualizer"
{
Properties
{
[HDR] _BaseColor ("Color", Color) = (1, 0, 0, 0.7)
_BorderWidth ("Border Width", Range(0.005, 0.15)) = 0.035
}
SubShader
{
Tags
{
"RenderType" = "Transparent"
"Queue" = "Transparent+200"
"RenderPipeline" = "UniversalPipeline"
}
Pass
{
Name "HitboxPass"
Blend SrcAlpha OneMinusSrcAlpha
ZWrite Off
Cull Off
ZTest LEqual
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_instancing
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
struct Attributes
{
float4 positionOS : POSITION;
float2 uv : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct Varyings
{
float4 positionCS : SV_POSITION;
float2 uv : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
CBUFFER_START(UnityPerMaterial)
float _BorderWidth;
CBUFFER_END
UNITY_INSTANCING_BUFFER_START(Props)
UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)
UNITY_INSTANCING_BUFFER_END(Props)
Varyings vert(Attributes input)
{
Varyings output = (Varyings)0;
UNITY_SETUP_INSTANCE_ID(input);
UNITY_TRANSFER_INSTANCE_ID(input, output);
output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
output.uv = input.uv;
return output;
}
half4 frag(Varyings input) : SV_Target
{
UNITY_SETUP_INSTANCE_ID(input);
half4 baseColor = UNITY_ACCESS_INSTANCED_PROP(Props, _BaseColor);
// Дистанция до края полигона (грани куба)
float2 dist = min(input.uv, 1.0 - input.uv);
float edgeDist = min(dist.x, dist.y);
// 1.0 если пиксель на краю грани, иначе 0.0
float isEdge = step(edgeDist, _BorderWidth);
half4 finalColor = baseColor;
// Внутри объема цвет прозрачный, на гранях — яркий и четкий
finalColor.a = lerp(baseColor.a * 0.25, saturate(baseColor.a * 1.5), isEdge);
finalColor.rgb = lerp(baseColor.rgb, baseColor.rgb * 1.3, isEdge);
return finalColor;
}
ENDHLSL
}
}
FallBack Off
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 17ecf4fbd19d4a63ad7b3b6a943166be
timeCreated: 1794178806
@@ -120,9 +120,14 @@ namespace Code.OfficeGame.BattleMode.Timeline
foreach (var (entityId, entity) in entities)
{
if (!_actionDecisionResults.TryGetValue(entityId, out var actionDecisionResult)
|| !_battleActionDatas.TryGetValue(entityId, out var battleActionData))
{
entity.EntityPreviewVisuals.ShowPreviewState(spawnStaticSnapshotGhost: false);
continue;
}
entity.EntityPreviewVisuals.ShowPreviewState(spawnStaticSnapshotGhost: true);
resultsSnapshots[entityId] = actionDecisionResult.GetSnapshot();
actionDataSnapshots[entityId] = battleActionData.GetSnapshot();
@@ -141,6 +146,11 @@ namespace Code.OfficeGame.BattleMode.Timeline
finally
{
RestoreFromSnapshots(entities, resultsSnapshots, actionDataSnapshots, entitySnapshots);
foreach (var (_, entity) in entities)
{
entity.EntityPreviewVisuals.HidePreviewState();
}
}
}