Pin hand to camera view and move camera closer
CI / test (push) Successful in 2m13s
Details
CI / test (push) Successful in 2m13s
Details
This commit is contained in:
parent
c2489b5452
commit
f2a01bae16
|
|
@ -53,11 +53,49 @@ namespace Vanguard.Editor
|
|||
var board = UnityEngine.Object.FindAnyObjectByType<Vanguard.Board3D.GameBoard3D>();
|
||||
var cards = UnityEngine.Object.FindObjectsByType<Vanguard.Board3D.Card3D>(
|
||||
FindObjectsInactive.Exclude);
|
||||
var success = board != null && cards.Length >= 10;
|
||||
var camera = Camera.main;
|
||||
var handVisible = 0;
|
||||
var handCards = 0;
|
||||
var fieldVisible = false;
|
||||
if (camera != null)
|
||||
{
|
||||
foreach (var card in cards)
|
||||
{
|
||||
var viewport = camera.WorldToViewportPoint(card.transform.position);
|
||||
if (card.Ref.Zone == Vanguard.Board3D.BoardZone.Hand)
|
||||
{
|
||||
handCards++;
|
||||
if (viewport.z > 0f
|
||||
&& viewport.x > 0.02f
|
||||
&& viewport.x < 0.98f
|
||||
&& viewport.y > 0.01f
|
||||
&& viewport.y < 0.55f)
|
||||
{
|
||||
handVisible++;
|
||||
}
|
||||
}
|
||||
else if (card.Ref.Zone == Vanguard.Board3D.BoardZone.Vanguard
|
||||
&& card.Ref.Player == 0
|
||||
&& viewport.z > 0f
|
||||
&& viewport.y > 0.05f
|
||||
&& viewport.y < 0.75f)
|
||||
{
|
||||
fieldVisible = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var success = board != null
|
||||
&& cards.Length >= 10
|
||||
&& handCards > 0
|
||||
&& handVisible == handCards
|
||||
&& fieldVisible;
|
||||
Debug.Log(
|
||||
"PLAYMAT_SMOKE success=" + success
|
||||
+ " board=" + (board != null)
|
||||
+ " cards=" + cards.Length
|
||||
+ " hand=" + handVisible + "/" + handCards
|
||||
+ " fieldVisible=" + fieldVisible
|
||||
+ " error=" + VanguardApp.LastInitializationError);
|
||||
|
||||
EditorApplication.Exit(success ? 0 : 1);
|
||||
|
|
|
|||
|
|
@ -54,12 +54,12 @@ namespace Vanguard.App
|
|||
camera.fieldOfView = theme == SceneTheme.Battle ? 50f : 52f;
|
||||
camera.transform.position = theme switch
|
||||
{
|
||||
SceneTheme.Battle => new Vector3(0f, 8.7f, -12.4f),
|
||||
SceneTheme.Battle => new Vector3(0f, 6.9f, -9.8f),
|
||||
SceneTheme.Login => new Vector3(0f, 2.4f, -9.5f),
|
||||
_ => new Vector3(0f, 3.2f, -10f),
|
||||
};
|
||||
camera.transform.rotation = theme == SceneTheme.Battle
|
||||
? Quaternion.Euler(34.4f, 0f, 0f)
|
||||
? Quaternion.Euler(35.2f, 0f, 0f)
|
||||
: Quaternion.Euler(8f, 0f, 0f);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,13 +36,19 @@ MonoBehaviour:
|
|||
handSpacing: 0.42
|
||||
handMaxWidth: 7.4
|
||||
handArc: 0.035
|
||||
handViewportY: 0.16
|
||||
handViewDistance: 3.6
|
||||
handViewMaxWidth: 2
|
||||
handViewScale: 0.9
|
||||
handViewRoll: 3.2
|
||||
handViewArc: 0.02
|
||||
guardianRowGap: 0.55
|
||||
guardianSpacing: 0.44
|
||||
guardianMaxWidth: 3.6
|
||||
damageX: 3.5
|
||||
pileX: 3.55
|
||||
cameraPosition: {x: 0, y: 8.7, z: -12.4}
|
||||
cameraTarget: {x: 0, y: 0, z: 0.3}
|
||||
cameraPosition: {x: 0, y: 6.9, z: -9.8}
|
||||
cameraTarget: {x: 0, y: 0, z: -0.15}
|
||||
cameraFov: 50
|
||||
orbitYawLimit: 32
|
||||
orbitPitchMin: 20
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ namespace Vanguard.Board3D
|
|||
public float smoothRotateSpeed = 14f;
|
||||
public float handHoverLift = 0.08f;
|
||||
|
||||
[Header("Рука")]
|
||||
[Header("Рука на столе (fallback без камеры)")]
|
||||
public float handCenterZ = -5.3f;
|
||||
public float handHeight = 1.12f;
|
||||
[Range(0f, 85f)] public float handTilt = 58f;
|
||||
|
|
@ -49,6 +49,16 @@ namespace Vanguard.Board3D
|
|||
public float handMaxWidth = 7.4f;
|
||||
public float handArc = 0.035f;
|
||||
|
||||
[Header("Рука в поле зрения")]
|
||||
[Tooltip("Доля высоты экрана для центра карт: 0 — низ, 0.5 — середина.")]
|
||||
[Range(0.05f, 0.45f)] public float handViewportY = 0.16f;
|
||||
[Tooltip("Расстояние от камеры до веера руки, метры.")]
|
||||
public float handViewDistance = 3.6f;
|
||||
public float handViewMaxWidth = 2.0f;
|
||||
public float handViewScale = 0.9f;
|
||||
public float handViewRoll = 3.2f;
|
||||
public float handViewArc = 0.02f;
|
||||
|
||||
[Header("Круг защиты")]
|
||||
public float guardianRowGap = 0.55f;
|
||||
public float guardianSpacing = 0.44f;
|
||||
|
|
@ -61,8 +71,8 @@ namespace Vanguard.Board3D
|
|||
public float pileX = 3.55f;
|
||||
|
||||
[Header("Камера")]
|
||||
public Vector3 cameraPosition = new Vector3(0f, 8.7f, -12.4f);
|
||||
public Vector3 cameraTarget = new Vector3(0f, 0f, 0.3f);
|
||||
public Vector3 cameraPosition = new Vector3(0f, 6.9f, -9.8f);
|
||||
public Vector3 cameraTarget = new Vector3(0f, 0f, -0.15f);
|
||||
public float cameraFov = 50f;
|
||||
public float orbitYawLimit = 32f;
|
||||
public float orbitPitchMin = 20f;
|
||||
|
|
@ -131,6 +141,12 @@ namespace Vanguard.Board3D
|
|||
snapRadius = Mathf.Clamp(snapRadius, 0.2f, 1.5f);
|
||||
handMaxWidth = Mathf.Max(1f, handMaxWidth);
|
||||
guardianMaxWidth = Mathf.Max(1f, guardianMaxWidth);
|
||||
handViewportY = Mathf.Clamp(handViewportY, 0.05f, 0.45f);
|
||||
handViewDistance = Mathf.Max(1f, handViewDistance);
|
||||
handViewMaxWidth = Mathf.Max(0.5f, handViewMaxWidth);
|
||||
handViewScale = Mathf.Clamp(handViewScale, 0.3f, 1.5f);
|
||||
handViewRoll = Mathf.Clamp(handViewRoll, 0f, 20f);
|
||||
handViewArc = Mathf.Max(0f, handViewArc);
|
||||
orbitPitchMax = Mathf.Clamp(orbitPitchMax, orbitPitchMin + 1f, 85f);
|
||||
zoomMax = Mathf.Max(zoomMin + 0.5f, zoomMax);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -149,11 +149,12 @@ namespace Vanguard.Board3D
|
|||
bool faceUp,
|
||||
bool rested,
|
||||
bool immediate,
|
||||
bool damageLayout = false)
|
||||
bool damageLayout = false,
|
||||
float scale = 1f)
|
||||
{
|
||||
SetState(baseRotation, faceUp, rested, damageLayout);
|
||||
_targetPosition = position;
|
||||
_targetScale = Vector3.one;
|
||||
_targetScale = Vector3.one * Mathf.Max(0.05f, scale);
|
||||
_immediate = immediate;
|
||||
|
||||
if (_dragging)
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ namespace Vanguard.Board3D
|
|||
private readonly BoardZoneView[] _damageZones = new BoardZoneView[2];
|
||||
private readonly HashSet<BoardSlotKey> _activeHighlights = new HashSet<BoardSlotKey>();
|
||||
private readonly HashSet<BoardSlotKey> _validDropSlots = new HashSet<BoardSlotKey>();
|
||||
private readonly List<HandCardBinding> _handBindings = new List<HandCardBinding>();
|
||||
private bool _guardianDropAllowed;
|
||||
|
||||
private PlaymatLayoutConfig _config;
|
||||
|
|
@ -150,6 +151,7 @@ namespace Vanguard.Board3D
|
|||
{
|
||||
CancelPointer();
|
||||
_hasRendered = false;
|
||||
_handBindings.Clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -176,7 +178,10 @@ namespace Vanguard.Board3D
|
|||
SetSelfPlayer(view.You);
|
||||
_view = view;
|
||||
state ??= new BoardRenderState();
|
||||
if (_camera == null)
|
||||
_camera = Camera.main;
|
||||
|
||||
_handBindings.Clear();
|
||||
var specs = new List<CardSpec>(40);
|
||||
BuildSpecs(view, state, specs);
|
||||
|
||||
|
|
@ -203,8 +208,17 @@ namespace Vanguard.Board3D
|
|||
spec.FaceUp,
|
||||
spec.Rested,
|
||||
immediateAll || changed,
|
||||
spec.DamageLayout);
|
||||
spec.DamageLayout,
|
||||
spec.Scale);
|
||||
card.SetHighlight(spec.Highlight);
|
||||
|
||||
if (spec.IsHand)
|
||||
{
|
||||
_handBindings.Add(new HandCardBinding(
|
||||
card,
|
||||
spec.HandIndex,
|
||||
spec.HandCount));
|
||||
}
|
||||
}
|
||||
|
||||
if (_cards.Count > 0)
|
||||
|
|
@ -228,6 +242,7 @@ namespace Vanguard.Board3D
|
|||
public void Clear()
|
||||
{
|
||||
ClearCards();
|
||||
_handBindings.Clear();
|
||||
_view = null;
|
||||
_hasRendered = false;
|
||||
}
|
||||
|
|
@ -597,7 +612,7 @@ namespace Vanguard.Board3D
|
|||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var card = self.Hand[i];
|
||||
var pose = _layout.GetHandPose(i, count);
|
||||
GetHandViewPose(i, count, out var position, out var rotation, out var scale);
|
||||
Color? highlight = null;
|
||||
if (state.SelectedHandInstanceIds.Contains(card.InstanceId))
|
||||
highlight = _config.selectedColor;
|
||||
|
|
@ -609,15 +624,99 @@ namespace Vanguard.Board3D
|
|||
Key = "H:" + card.InstanceId,
|
||||
Ref = BoardCardRef.Hand(self.Index, i, card),
|
||||
Art = ResolveArt(card.PrintingId),
|
||||
Position = pose.Position,
|
||||
BaseRotation = Quaternion.Euler(_config.handTilt, pose.Yaw, 0f),
|
||||
Position = position,
|
||||
BaseRotation = rotation,
|
||||
FaceUp = true,
|
||||
Rested = false,
|
||||
Scale = scale,
|
||||
IsHand = true,
|
||||
HandIndex = i,
|
||||
HandCount = count,
|
||||
Highlight = highlight,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Рука висит в поле зрения камеры снизу по центру и всегда смотрит
|
||||
/// на игрока. При отсутствии камеры используется старый настольный
|
||||
/// вариант — для EditMode-проверок.
|
||||
/// </summary>
|
||||
private void GetHandViewPose(
|
||||
int index,
|
||||
int count,
|
||||
out Vector3 position,
|
||||
out Quaternion rotation,
|
||||
out float scale)
|
||||
{
|
||||
if (_camera == null)
|
||||
_camera = Camera.main;
|
||||
|
||||
if (_camera == null)
|
||||
{
|
||||
var fallback = _layout.GetHandPose(index, count);
|
||||
position = fallback.Position;
|
||||
rotation = Quaternion.Euler(_config.handTilt, fallback.Yaw, 0f);
|
||||
scale = 1f;
|
||||
return;
|
||||
}
|
||||
|
||||
var safeCount = Mathf.Max(1, count);
|
||||
var step = _config.handSpacing;
|
||||
if (safeCount > 1)
|
||||
step = Mathf.Min(step, _config.handViewMaxWidth / (safeCount - 1));
|
||||
|
||||
var offset = index - (safeCount - 1) * 0.5f;
|
||||
var halfHeight = Mathf.Tan(
|
||||
_camera.fieldOfView * 0.5f * Mathf.Deg2Rad) * _config.handViewDistance;
|
||||
var x = offset * step;
|
||||
var y = (_config.handViewportY - 0.5f) * 2f * halfHeight
|
||||
+ (1f - Mathf.Abs(offset) / safeCount) * _config.handViewArc;
|
||||
var z = _config.handViewDistance
|
||||
+ Mathf.Abs(offset) * _config.handViewArc;
|
||||
|
||||
position = _camera.transform.TransformPoint(new Vector3(x, y, z));
|
||||
var roll = -offset * _config.handViewRoll;
|
||||
rotation = _camera.transform.rotation
|
||||
* Quaternion.Euler(0f, 0f, roll)
|
||||
* Quaternion.Euler(-90f, 0f, 0f);
|
||||
scale = _config.handViewScale;
|
||||
}
|
||||
|
||||
private void UpdateHandView()
|
||||
{
|
||||
if (_handBindings.Count == 0)
|
||||
return;
|
||||
|
||||
if (_camera == null)
|
||||
{
|
||||
_camera = Camera.main;
|
||||
if (_camera == null)
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var binding in _handBindings)
|
||||
{
|
||||
if (binding.Card == null || binding.Card.IsDragging)
|
||||
continue;
|
||||
|
||||
GetHandViewPose(
|
||||
binding.Index,
|
||||
binding.Count,
|
||||
out var position,
|
||||
out var rotation,
|
||||
out var scale);
|
||||
binding.Card.SetPose(
|
||||
position,
|
||||
rotation,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
scale);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdatePiles(MatchView view)
|
||||
{
|
||||
var self = view.Players.FirstOrDefault(player => player.Index == view.You);
|
||||
|
|
@ -717,6 +816,8 @@ namespace Vanguard.Board3D
|
|||
|
||||
private void Update()
|
||||
{
|
||||
UpdateHandView();
|
||||
|
||||
var delta = Time.unscaledDeltaTime;
|
||||
foreach (var card in _cards.Values)
|
||||
{
|
||||
|
|
@ -909,6 +1010,9 @@ namespace Vanguard.Board3D
|
|||
var targetValue = target.Value;
|
||||
var targetRef = targetValue.ToRef();
|
||||
|
||||
if (source.IsHand)
|
||||
_handBindings.RemoveAll(binding => binding.Card == card);
|
||||
|
||||
// Атака — это клик по цели, карта остаётся на своём круге.
|
||||
if ((source.IsField && targetValue.Player != source.Player)
|
||||
|| (source.IsHand && targetValue.Player != source.Player
|
||||
|
|
@ -1120,6 +1224,20 @@ namespace Vanguard.Board3D
|
|||
}
|
||||
}
|
||||
|
||||
private sealed class HandCardBinding
|
||||
{
|
||||
public HandCardBinding(Card3D card, int index, int count)
|
||||
{
|
||||
Card = card;
|
||||
Index = index;
|
||||
Count = count;
|
||||
}
|
||||
|
||||
public Card3D Card { get; }
|
||||
public int Index { get; }
|
||||
public int Count { get; }
|
||||
}
|
||||
|
||||
private sealed class CardSpec
|
||||
{
|
||||
public string Key;
|
||||
|
|
@ -1130,6 +1248,10 @@ namespace Vanguard.Board3D
|
|||
public bool FaceUp;
|
||||
public bool Rested;
|
||||
public bool DamageLayout;
|
||||
public float Scale = 1f;
|
||||
public bool IsHand;
|
||||
public int HandIndex;
|
||||
public int HandCount;
|
||||
public Color? Highlight;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,6 +67,53 @@ namespace Vanguard.Tests
|
|||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Hand_Is_Anchored_To_Camera_Bottom_Center_And_Faces_Player()
|
||||
{
|
||||
var config = PlaymatLayoutConfig.CreateDefault();
|
||||
var cameraGo = new GameObject("Main Camera", typeof(Camera))
|
||||
{
|
||||
tag = "MainCamera",
|
||||
};
|
||||
var camera = cameraGo.GetComponent<Camera>();
|
||||
camera.fieldOfView = config.cameraFov;
|
||||
camera.transform.position = config.cameraPosition;
|
||||
camera.transform.rotation = Quaternion.LookRotation(
|
||||
config.cameraTarget - config.cameraPosition,
|
||||
Vector3.up);
|
||||
|
||||
var board = GameBoard3D.Create(null, config);
|
||||
try
|
||||
{
|
||||
board.SetVisible(true);
|
||||
board.Render(BuildView(you: 0), new BoardRenderState());
|
||||
|
||||
var hand = ActiveCards()
|
||||
.Where(card => card.Ref.Zone == BoardZone.Hand)
|
||||
.ToList();
|
||||
Assert.AreEqual(2, hand.Count);
|
||||
|
||||
foreach (var card in hand)
|
||||
{
|
||||
var viewport = camera.WorldToViewportPoint(card.transform.position);
|
||||
Assert.Greater(viewport.z, 0f, "Hand card must be in front of the camera");
|
||||
Assert.That(viewport.x, Is.InRange(0.15f, 0.85f));
|
||||
Assert.That(viewport.y, Is.InRange(0.01f, 0.55f));
|
||||
|
||||
var toCamera = (camera.transform.position - card.transform.position).normalized;
|
||||
Assert.Greater(
|
||||
Vector3.Dot(card.transform.up, toCamera),
|
||||
0.8f,
|
||||
"Hand card must face the camera");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Object.DestroyImmediate(board.gameObject);
|
||||
Object.DestroyImmediate(cameraGo);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Pool_Reuses_Cards_Between_Snapshots()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -854,15 +854,19 @@ Battle; Boot и общие сервисы остаются персистент
|
|||
- `GameBoard3D` строит стол, круги, зоны, стопки и веер руки; raycast-клики,
|
||||
drag&drop на плоскость стола с подъёмом, подсветкой допустимых кругов и
|
||||
индикатором точки броска. Ошибка интента возвращает карту на место.
|
||||
- Камера: `BoardCameraRig` — ПКМ орбита (±32°), СКМ панорама, колесо зум
|
||||
- Камера приближена: позиция `(0, 6.9, -9.8)`, цель `(0, 0, -0.15)`, FOV 50.
|
||||
Рука не лежит на столе, а закреплена в экране снизу по центру
|
||||
(`handViewportY`, `handViewDistance`) и всегда развёрнута лицом к игроку;
|
||||
при орбите камеры веер следует за полем зрения, драг из руки уходит на стол.
|
||||
- `BoardCameraRig` — ПКМ орбита (±32°), СКМ панорама, колесо зум
|
||||
(9.5–16); ограничения в конфиге.
|
||||
- Душа: клик по стопке или кнопка «Душа: n» в верхней панели открывает
|
||||
оверлей со списком карт.
|
||||
- Песочница: `Assets/Vanguard/Scenes/GameBoard3D.unity` +
|
||||
`PlaymatSandbox`; собирается `Vanguard → Rebuild Playmat Sandbox`,
|
||||
проверяется `SceneSmoke.RunPlaymat`.
|
||||
- Проверки: EditMode **17/17** (геометрия, зеркальность, правила, рендер
|
||||
3D-карт, пул), `SCENE_SMOKE success=True`,
|
||||
`PLAYMAT_SMOKE success=True cards=20`.
|
||||
- Проверки: EditMode **18/18** (геометрия, зеркальность, правила, рендер
|
||||
3D-карт, пул, рука в поле зрения), `SCENE_SMOKE success=True`,
|
||||
`PLAYMAT_SMOKE success=True cards=20 hand=5/5 fieldVisible=True`.
|
||||
- Ограничения: retire/ghost-анимации старого 2D-поля пока не перенесены;
|
||||
модели карт/стола — процедурные примитивы.
|
||||
|
|
|
|||
|
|
@ -65,7 +65,9 @@
|
|||
остаются uGUI. Карты — тонкие 3D-объекты с артом, рубашкой, торцом и
|
||||
коллайдером. Rest (атака/поддержка) — поворот по часовой стрелке вокруг Y,
|
||||
урон лежит на боку против часовой; закрытость — переворот вокруг X.
|
||||
Драг-н-дроп на плоскость стола с подсветкой целей и плавным снапом.
|
||||
Рука закреплена в поле зрения камеры снизу по центру и всегда смотрит
|
||||
на игрока, а не лежит на столе. Драг-н-дроп на плоскость стола с
|
||||
подсветкой целей и плавным снапом; камера приближена.
|
||||
- Поле: у каждого игрока два ряда `R V R` / `R R R` (V + 5 RC), у
|
||||
противника зеркально; между полями общий круг защиты; душа — стопка под
|
||||
V; колода, дроп и урон — 3D-стопки/карты. Локальный игрок всегда снизу,
|
||||
|
|
@ -131,10 +133,11 @@ curl http://127.0.0.1:18080/health
|
|||
-executeMethod Vanguard.Editor.SceneSmoke.RunPlaymat
|
||||
```
|
||||
|
||||
Клиентские проверки: **17/17** EditMode (сборка всех экранов, импорт арта,
|
||||
Клиентские проверки: **18/18** EditMode (сборка всех экранов, импорт арта,
|
||||
перевод статических/динамических строк, live-переключение языка, рендер
|
||||
3D-доски с уроном и отдыхающей картой, геометрия поля и правила
|
||||
перемещения), `SCENE_SMOKE success=True`, `PLAYMAT_SMOKE success=True cards=20`.
|
||||
3D-доски с уроном и отдыхающей картой, геометрия поля, правила
|
||||
перемещения, рука в поле зрения камеры), `SCENE_SMOKE success=True`,
|
||||
`PLAYMAT_SMOKE success=True cards=20 hand=5/5 fieldVisible=True`.
|
||||
|
||||
`SceneSmoke` заменяет PlayMode-тест загрузки сцен: Single-загрузка сцены
|
||||
ломает стандартный PlayMode test runner. `SceneBuilder` пересобирает сцены:
|
||||
|
|
|
|||
Loading…
Reference in New Issue