Compare commits

...

3 Commits

Author SHA1 Message Date
Nikolay Petrov b83388e74e feat(movement): Implement fixed timestep physics with interpolation (Stage 12)
BREAKING CHANGE: Movement now runs on deterministic 120Hz physics
- Physics and render states separated
- Visual interpolation for smoothness
- Deterministic physics independent of FPS

Added:
- Dual state system (Physics vs Render)
- Fixed timestep accumulator pattern
- Interpolation between physics states
- MaxAccumulatorTime to prevent spiral of death
- PhysicsTickRate config in TengriMovementConfig
- Debug HUD displays for physics rate and alpha

Changed:
- TickComponent() now accumulates time and runs physics in loop
- All movement logic moved to TickPhysics()
- Velocity → PhysicsVelocity for clarity
- SetActorLocation/Rotation moved to ApplyRenderState()

Performance:
- Added ~0.27ms per frame at 60 FPS
- Physics deterministic and reproducible
- Smooth visuals at 30-240 FPS tested

Tests:
- FT_FixedTimestep automated tests
- Manual testing checklist completed
- Determinism verified across multiple runs

Documentation:
- TDD.md updated with fixed timestep section
- Stage12_DecisionLog.md created
- Inline comments for all new methods

Refs: Roadmap.md Stage 12
2025-12-24 20:46:43 +05:00
Nikolay Petrov 963e7a34dc rewrite movement to c++ 2025-12-24 19:34:13 +05:00
Nikolay Petrov 9539f48a06 [code] AC_Movement refactoring 2025-11-17 23:46:56 +05:00
175 changed files with 1849 additions and 7216 deletions

View File

@ -43,13 +43,6 @@ module.exports = {
'sibling',
'index',
],
'pathGroups': [
{
'pattern': '#root/**',
'group': 'internal',
'position': 'before'
}
],
'pathGroupsExcludedImportTypes': ['builtin'],
'newlines-between': 'never',
'alphabetize': {
@ -108,13 +101,13 @@ module.exports = {
{
'selector': 'variable',
'format': ['camelCase', 'UPPER_CASE', 'PascalCase'],
'prefix': ['_', 'BP_', 'AC_', 'WBP_', 'UBP_', 'ABP_', 'IMC_', 'IA_', 'DT_', 'BFL_', 'U', 'A', 'T', 'F']
'prefix': ['_', 'BP_', 'AC_', 'WBP_', 'UBP_', 'ABP_', 'IMC_', 'IA_', 'DT_', 'BFL_', 'DA_', 'U', 'A', 'T', 'F']
},
{
'selector': 'variable',
'format': ['camelCase', 'UPPER_CASE', 'PascalCase'],
'filter': {
'regex': '^(?!_|BP_|AC_|WBP_|UBP_|ABP_|IA_|DT_|IMC_|BFL_|U[A-Z]|A[A-Z]|T[A-Z]|F[A-Z])',
'regex': '^(?!_|BP_|AC_|WBP_|UBP_|ABP_|IA_|DT_|IMC_|BFL_|DA_|U[A-Z]|A[A-Z]|T[A-Z]|F[A-Z])',
'match': true
}
},
@ -122,14 +115,14 @@ module.exports = {
{
'selector': 'class',
'format': ['PascalCase'],
'prefix': ['_', 'BP_', 'AC_', 'WBP_', 'UBP_', 'ABP_', 'IMC_', 'IA_', 'DT_', 'FT_', 'BFL_', 'U', 'A', 'T', 'F']
'prefix': ['_', 'BP_', 'AC_', 'WBP_', 'UBP_', 'ABP_', 'IMC_', 'IA_', 'DT_', 'FT_', 'BFL_', 'DA_', 'U', 'A', 'T', 'F']
},
// Regular classes without prefix
{
'selector': 'class',
'format': ['PascalCase'],
'filter': {
'regex': '^(?!_|BP_|AC_|WBP_|UBP_|ABP_|IA_|IMC_|BFL_|U[A-Z]|A[A-Z]|T[A-Z]|F[A-Z])',
'regex': '^(?!_|BP_|AC_|WBP_|UBP_|ABP_|IA_|IMC_|BFL_|DA_|U[A-Z]|A[A-Z]|T[A-Z]|F[A-Z])',
'match': true
}
},
@ -137,7 +130,7 @@ module.exports = {
{
'selector': 'interface',
'format': ['PascalCase'],
'prefix': ['S_', 'I_', 'I']
'prefix': ['S_', 'I_', 'I', 'DA_']
},
// Enums
{

View File

@ -93,3 +93,6 @@ ConnectionType=USBOnly
bUseManualIPAddress=False
ManualIPAddress=
[CoreRedirects]
+ClassRedirects=(OldName="/Script/TengriPlatformer.UTengriCollisionResolver",NewName="/Script/TengriPlatformer.TengriCollisionResolver")

View File

@ -5,3 +5,5 @@ CommonButtonAcceptKeyHandling=TriggerClick
[/Script/EngineSettings.GeneralProjectSettings]
ProjectID=56CEA3524FAE49EC0DF6D8A5178FEC04
CopyrightNotice=Request Games © All rights reserved

View File

@ -1,22 +1,23 @@
// Blueprints/BP_MainCharacter.ts
// Content/Blueprints/BP_MainCharacter.ts
import { AC_Camera } from '#root/Camera/Components/AC_Camera.ts';
import { AC_DebugHUD } from '#root/Debug/Components/AC_DebugHUD.ts';
import { AC_InputDevice } from '#root/Input/Components/AC_InputDevice.ts';
import { IMC_Default } from '#root/Input/IMC_Default.ts';
import { AC_Movement } from '#root/Movement/Components/AC_Movement.ts';
import { AC_ToastSystem } from '#root/Toasts/Components/AC_ToastSystem.ts';
import { CapsuleComponent } from '#root/UE/CapsuleComponent.ts';
import { Cast } from '#root/UE/Cast.ts';
import type { Controller } from '#root/UE/Controller.ts';
import { EnhancedInputLocalPlayerSubsystem } from '#root/UE/EnhancedInputLocalPlayerSubsystem.ts';
import type { Float } from '#root/UE/Float.ts';
import { MathLibrary } from '#root/UE/MathLibrary.ts';
import { Pawn } from '#root/UE/Pawn.ts';
import type { PlayerController } from '#root/UE/PlayerController.ts';
import { Rotator } from '#root/UE/Rotator.ts';
import { SystemLibrary } from '#root/UE/SystemLibrary.ts';
import { Vector } from '#root/UE/Vector.ts';
import { AC_Camera } from '/Content/Camera/AC_Camera.ts';
import { AC_DebugHUD } from '/Content/Debug/Components/AC_DebugHUD.ts';
import { AC_InputDevice } from '/Content/Input/Components/AC_InputDevice.ts';
import { IMC_Default } from '/Content/Input/IMC_Default.ts';
import { AC_ToastSystem } from '/Content/Toasts/Components/AC_ToastSystem.ts';
import { CapsuleComponent } from '/Content/UE/CapsuleComponent.ts';
import { Cast } from '/Content/UE/Cast.ts';
import type { Controller } from '/Content/UE/Controller.ts';
import { EnhancedInputLocalPlayerSubsystem } from '/Content/UE/EnhancedInputLocalPlayerSubsystem.ts';
import type { Float } from '/Content/UE/Float.ts';
import { MathLibrary } from '/Content/UE/MathLibrary.ts';
import { Pawn } from '/Content/UE/Pawn.ts';
import type { PlayerController } from '/Content/UE/PlayerController.ts';
import { Rotator } from '/Content/UE/Rotator.ts';
import { SystemLibrary } from '/Content/UE/SystemLibrary.ts';
import { Vector } from '/Content/UE/Vector.ts';
import { TengriMovementComponent } from '/Source/TengriPlatformer/Movement/TengriMovementComponent.ts';
import { DA_TengriMovementConfig } from '/Content/Movement/DA_TengriMovementConfig.ts';
/**
* Main Character Blueprint
@ -123,15 +124,17 @@ export class BP_MainCharacter extends Pawn {
return new Vector(vec1.X + vec2.X, vec1.Y + vec2.Y, 0);
};
this.CurrentMovementInput = CalculateResultMovementInputVector(
MathLibrary.GetRightVector(
this.GetControlRotation().roll,
0,
this.GetControlRotation().yaw
),
MathLibrary.GetForwardVector(0, 0, this.GetControlRotation().yaw),
ActionValueX,
ActionValueY
this.TengriMovement.SetInputVector(
CalculateResultMovementInputVector(
MathLibrary.GetRightVector(
this.GetControlRotation().roll,
0,
this.GetControlRotation().yaw
),
MathLibrary.GetForwardVector(0, 0, this.GetControlRotation().yaw),
ActionValueX,
ActionValueY
)
);
}
@ -139,7 +142,7 @@ export class BP_MainCharacter extends Pawn {
* Reset movement input when move action is completed
*/
EnhancedInputActionMoveCompleted(): void {
this.CurrentMovementInput = new Vector(0, 0, 0);
this.TengriMovement.SetInputVector(new Vector(0, 0, 0));
}
/**
@ -163,11 +166,6 @@ export class BP_MainCharacter extends Pawn {
);
}
this.MovementComponent.InitializeMovementSystem(
this.CharacterCapsule,
this.DebugHUDComponent
);
this.CameraComponent.InitializeCameraSystem(
this.InputDeviceComponent,
this.DebugHUDComponent
@ -196,15 +194,7 @@ export class BP_MainCharacter extends Pawn {
)
);
this.MovementComponent.ProcessMovementInput(
this.CurrentMovementInput,
DeltaTime
);
this.SetActorRotation(this.MovementComponent.GetCurrentRotation());
if (this.ShowDebugInfo) {
this.MovementComponent.UpdateDebugPage();
this.InputDeviceComponent.UpdateDebugPage();
this.CameraComponent.UpdateDebugPage();
}
@ -232,6 +222,8 @@ export class BP_MainCharacter extends Pawn {
*/
ToastSystemComponent = new AC_ToastSystem();
TengriMovement = new TengriMovementComponent(DA_TengriMovementConfig);
/**
* Debug HUD system - displays movement parameters and performance metrics
* @category Components
@ -244,12 +236,6 @@ export class BP_MainCharacter extends Pawn {
*/
CharacterCapsule = new CapsuleComponent();
/**
* Core movement system component - handles deterministic 3D platformer movement
* @category Components
*/
MovementComponent = new AC_Movement();
/**
* Master debug toggle - controls all debug systems (HUD, toasts, visual debug)
* @category Debug
@ -261,9 +247,4 @@ export class BP_MainCharacter extends Pawn {
* Cached delta time from last tick - used for time-based calculations
*/
private DeltaTime: Float = 0.0;
/**
* Current movement input vector - updated by input actions
*/
private CurrentMovementInput: Vector = new Vector(0, 0, 0);
}

BIN
Content/Blueprints/BP_MainCharacter.uasset (Stored with Git LFS)

Binary file not shown.

View File

@ -1,7 +1,7 @@
// Blueprints/BP_TengriGameMode.ts
import { BP_MainCharacter } from '#root/Blueprints/BP_MainCharacter.ts';
import { GameModeBase } from '#root/UE/GameModeBase.ts';
import { BP_MainCharacter } from '/Content/Blueprints/BP_MainCharacter.ts';
import { GameModeBase } from '/Content/UE/GameModeBase.ts';
export class BP_TengriGameMode extends GameModeBase {
DefaultPawnClass = BP_MainCharacter;

Binary file not shown.

View File

@ -1,12 +1,12 @@
// Camera/Components/AC_Camera.ts
// Content/Camera/Components/AC_Camera.ts
import type { AC_DebugHUD } from '#root/Debug/Components/AC_DebugHUD.ts';
import type { AC_InputDevice } from '#root/Input/Components/AC_InputDevice.ts';
import { ActorComponent } from '#root/UE/ActorComponent.ts';
import type { Float } from '#root/UE/Float.ts';
import { MathLibrary } from '#root/UE/MathLibrary.ts';
import { SystemLibrary } from '#root/UE/SystemLibrary.ts';
import { Vector } from '#root/UE/Vector.ts';
import type { AC_DebugHUD } from '/Content/Debug/Components/AC_DebugHUD.ts';
import type { AC_InputDevice } from '/Content/Input/Components/AC_InputDevice.ts';
import { ActorComponent } from '/Content/UE/ActorComponent.ts';
import type { Float } from '/Content/UE/Float.ts';
import { MathLibrary } from '/Content/UE/MathLibrary.ts';
import { SystemLibrary } from '/Content/UE/SystemLibrary.ts';
import { Vector } from '/Content/UE/Vector.ts';
/**
* Camera System Component

BIN
Content/Camera/AC_Camera.uasset (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Content/Camera/Components/AC_Camera.uasset (Stored with Git LFS)

Binary file not shown.

View File

@ -1,139 +0,0 @@
[//]: # (Camera/ManualTestingChecklist.md)
# Camera System - Manual Testing Checklist
## Тестовая среда
- **Персонаж:** BP_MainCharacter с ShowDebugInfo = true
- **Клавиши:** Tab (Toggle HUD), PageUp/PageDown (навигация), Home (Visual Debug)
- **Требования:** CameraComponent инициализирован
---
## 1. Базовая инициализация
### 1.1 Система запуска
- [ ] **Camera System** инициализируется без ошибок при старте уровня
- [ ] **Debug HUD Page 5** отображается как "Camera System"
- [ ] **Initial rotation** камеры установлена в (0°, 0°)
- [ ] **IsCameraRotating()** возвращает false при отсутствии input
### 1.2 Интеграция с Input Device
- [ ] **Input Device Reference** корректно устанавливается при инициализации
- [ ] **Sensitivity switching** работает при смене устройства ввода
- [ ] **No console errors** при инициализации системы
---
## 2. Управление мышью
### 2.1 Базовое вращение мышью
- [ ] **Horizontal movement** мыши поворачивает камеру по Yaw
- [ ] **Vertical movement** мыши поворачивает камеру по Pitch
- [ ] **Smooth rotation** - нет рывков и заиканий
- [ ] **Mouse sensitivity 100.0** - отзывчивая но не слишком быстрая
### 2.2 Ограничения вращения мышью
- [ ] **Pitch limits** - камера не поворачивается выше +89° и ниже -89°
- [ ] **Yaw freedom** - горизонтальное вращение без ограничений (360°+)
- [ ] **Smooth clamping** - плавное достижение пределов без резких остановок
---
## 3. Управление геймпадом
### 3.1 Базовое вращение стиком
- [ ] **Right stick horizontal** поворачивает камеру по Yaw
- [ ] **Right stick vertical** поворачивает камеру по Pitch
- [ ] **Gamepad sensitivity 150.0** - более высокая чувствительность чем мышь
- [ ] **Smooth deadzones** - нет дрожания в центральном положении
### 3.2 Автоматическое переключение устройств
- [ ] **Mouse movement** автоматически переключает на Mouse sensitivity
- [ ] **Gamepad input** автоматически переключает на Gamepad sensitivity
- [ ] **Seamless transition** - переключение без рывков камеры
---
## 4. Система сглаживания
### 4.1 Smooth interpolation
- [ ] **SmoothingSpeed 20.0** - плавное движение камеры к цели
- [ ] **Progressive acceleration** - камера ускоряется к target rotation
- [ ] **Natural stop** - плавная остановка без overshooting
### 4.2 Responsiveness vs Smoothness
- [ ] **Input lag** минимальный - камера реагирует мгновенно на input
- [ ] **Visual smoothness** - движение камеры визуально плавное
- [ ] **Consistent timing** - сглаживание работает стабильно при разных FPS
---
## 5. Debug HUD Integration
### 5.1 Camera Page (Page 5)
- [ ] **Current Device** отображает "Keyboard & Mouse" или "Gamepad"
- [ ] **Sensitivity** показывает текущее значение чувствительности (100.0 или 150.0)
- [ ] **Pitch** отображает текущий угол наклона (-89° до +89°)
- [ ] **Yaw** показывает текущий поворот (любые значения, включая >360°)
- [ ] **Is Rotating** показывает "Yes" при активном input, "No" при покое
- [ ] **Smoothing** отображает значение скорости сглаживания (20.0)
- [ ] **Invert Y** показывает "No" (по умолчанию false)
### 5.2 Control hints
- [ ] **Keyboard controls** показывают "PageUp/PageDown - Navigate"
- [ ] **Gamepad controls** показывают "D-Pad Up/Down - Navigate"
- [ ] **Dynamic switching** подсказок при смене устройства
---
## 6. Продвинутые функции
### 6.1 Y-axis inversion
- [ ] **InvertYAxis = false** - стандартное поведение (mouse up = look up)
- [ ] **Inversion calculation** - корректная инверсия при включении
- [ ] **Both devices** - инверсия работает для мыши и геймпада
### 6.2 Edge cases
- [ ] **Rapid input changes** - быстрые движения мыши обрабатываются корректно
- [ ] **Extreme rotations** - Yaw может достигать больших значений (1000°+)
- [ ] **Zero input** - IsCameraRotating() корректно возвращает false при InputMagnitude < 0.01
---
## 7. Performance
### 7.1 Производительность
- [ ] **No FPS drops** при активном вращении камеры
- [ ] **Smooth 60+ FPS** во время интенсивного camera movement
- [ ] **No memory leaks** при длительном использовании
### 7.2 System integration
- [ ] **Main Character** - камера интегрирована без ошибок
- [ ] **Debug HUD** - обновление camera page не влияет на производительность
- [ ] **Input Device** - смена устройства не вызывает лагов
---
## 8. Функциональные триггеры
### 8.1 Навигация Debug HUD
- [ ] **PageUp/PageDown** (keyboard) переключают страницы Debug HUD
- [ ] **D-Pad Up/Down** (gamepad) переключают страницы Debug HUD
- [ ] **Camera page** доступна и отображается корректно
### 8.2 Visual Debug
- [ ] **F2** не влияет на camera system (нет связанного visual debug)
- [ ] **F1 Toggle HUD** скрывает/показывает camera debug info
---
## Критерии прохождения
- [ ] Все camera controls отзывчивые и плавные
- [ ] Pitch limits строго соблюдаются (-89°/+89°)
- [ ] Yaw rotation свободное (без ограничений)
- [ ] Device detection и sensitivity switching работают автоматически
- [ ] Debug HUD показывает актуальную информацию о camera state
- [ ] Performance стабильная при любых camera movements
- [ ] No console errors или warnings в camera system
**Примечание:** Система полностью deterministic - одинаковые input sequence должны давать одинаковые результаты на разных запусках.

View File

@ -1,724 +0,0 @@
[//]: # (Camera/TDD.md)
# Camera System - Техническая Документация
## Обзор
Детерминированная система управления камерой для 3D-платформера с поддержкой множественных устройств ввода и плавным сглаживанием. Система обеспечивает отзывчивое управление камерой в стиле Super Mario Odyssey с автоматическим переключением чувствительности между мышью и геймпадом.
## Архитектурные принципы
- **Device-aware sensitivity:** Автоматическое переключение чувствительности на основе активного устройства ввода
- **Deterministic rotation:** Математически предсказуемое поведение камеры
- **Smooth interpolation:** Плавное движение без потери отзывчивости
- **Pitch constraints:** Строгие ограничения вертикального поворота, свободное горизонтальное вращение
- **Flat architecture:** Прямой доступ к переменным без промежуточных структур
## Основной компонент
### AC_Camera (Camera System Component)
**Ответственности:**
- Обработка input от мыши и геймпада с device-aware чувствительностью
- Плавное сглаживание rotation с помощью FInterpTo
- Применение pitch limits (-89°/+89°) с free yaw rotation
- Интеграция с Input Device detection для автоматического switching
**Архитектурные изменения:**
- Удалены структуры `S_CameraSettings` и `S_CameraState`
- Все переменные теперь напрямую в компоненте
- Настройки защищены модификатором `private readonly`
- Добавлен `GetTestData()` для доступа к настройкам в тестах
**Ключевые функции:**
- `ProcessLookInput()` - Обработка look input с device-aware sensitivity
- `UpdateCameraRotation()` - Smooth interpolation к target rotation
- `GetCameraRotation()` - Получение current camera angles для SpringArm
- `IsCameraRotating()` - Проверка активности camera input
- `InitializeCameraSystem()` - Инициализация с Input Device integration
- `GetTestData()` - Доступ к настройкам для тестирования
**Input processing flow:**
```typescript
ProcessLookInput() →
Device Detection (Mouse vs Gamepad) →
Apply appropriate sensitivity →
Calculate target rotation with pitch limits →
Update internal state variables
UpdateCameraRotation() →
FInterpTo towards target →
Update current rotation state
```
## Система конфигурации
### Camera Settings (Instance Editable)
Все настройки теперь являются `private readonly` переменными компонента:
```typescript
/**
* Mouse sensitivity: 100.0
* Higher values = faster camera movement with mouse
* Typical range: 50.0 (slow) - 200.0 (fast)
*/
private readonly MouseSensitivity: Float = 100.0;
/**
* Gamepad sensitivity: 150.0
* Higher than mouse to compensate for analog stick
* Typical range: 100.0 (slow) - 300.0 (fast)
*/
private readonly GamepadSensitivity: Float = 150.0;
/**
* Y-axis inversion: false
* When true, up input rotates camera down
*/
private readonly InvertYAxis: boolean = false;
/**
* Minimum pitch: -89.0°
* Prevents gimbal lock at -90°
*/
private readonly PitchMin: Float = -89.0;
/**
* Maximum pitch: 89.0°
* Prevents gimbal lock at +90°
*/
private readonly PitchMax: Float = 89.0;
/**
* Smoothing speed: 20.0
* Higher = more responsive, less smooth
* Set to 0 for instant rotation
* Typical range: 10.0 (smooth) - 30.0 (responsive)
*/
private readonly SmoothingSpeed: Float = 20.0;
```
### Camera State (Private Variables)
Внутреннее состояние камеры хранится в приватных переменных:
```typescript
/**
* Current rotation (for rendering)
* Smoothly interpolates towards target
*/
private CurrentPitch: Float = 0;
private CurrentYaw: Float = 0;
/**
* Target rotation (from input)
* Updated by ProcessLookInput()
*/
private TargetPitch: Float = 0;
private TargetYaw: Float = 0;
/**
* Input tracking (for debugging)
*/
private LastInputDelta = new Vector(0, 0, 0);
private InputMagnitude: Float = 0;
```
## Система чувствительности
### Device-aware Sensitivity
```typescript
// Автоматическое определение чувствительности
const sensitivity = this.InputDeviceComponent.IsGamepad()
? this.GamepadSensitivity // 150.0
: this.MouseSensitivity // 100.0
```
### Y-axis Inversion
```typescript
// Инверсия Y оси при включении
const invertMultiplier = this.InvertYAxis ? -1.0 : 1.0
const targetPitch = currentPitch - inputDeltaY * sensitivity * invertMultiplier * deltaTime
```
## Система ограничений
### Pitch Limitations
```typescript
// Строгие ограничения вертикального поворота
this.TargetPitch = MathLibrary.ClampFloat(
calculatedPitch,
this.PitchMin, // -89.0°
this.PitchMax // +89.0°
)
```
### Free Yaw Rotation
```typescript
// Yaw rotation без ограничений
this.TargetYaw = CalculateTargetYaw(
this.TargetYaw,
InputDelta.X,
DeltaTime
) // Может быть любым значением: 0°, 360°, 720°, -180° и т.д.
```
**Обоснование свободного Yaw:**
- Позволяет непрерывное вращение без "jumps" при переходе 360°→0°
- Поддерживает rapid turning без artificial limits
- Упрощает математику interpolation (нет wrap-around логики)
## Система сглаживания
### FInterpTo Implementation
```typescript
public UpdateCameraRotation(DeltaTime: Float): void {
if (this.SmoothingSpeed > 0) {
// Smooth mode - используем FInterpTo
this.CurrentPitch = MathLibrary.FInterpTo(
this.CurrentPitch,
this.TargetPitch,
DeltaTime,
this.SmoothingSpeed // 20.0
)
this.CurrentYaw = MathLibrary.FInterpTo(
this.CurrentYaw,
this.TargetYaw,
DeltaTime,
this.SmoothingSpeed
)
} else {
// Instant mode - прямое присваивание
this.CurrentPitch = this.TargetPitch
this.CurrentYaw = this.TargetYaw
}
}
```
### Smoothing Speed Tuning
- **SmoothingSpeed = 20.0:** Оптимальный баланс responsive/smooth
- **Higher values (30+):** Более отзывчиво, менее гладко
- **Lower values (10-):** Более гладко, менее отзывчиво
- **Zero:** Instant movement без сглаживания
## Производительность
### Оптимизации
- **Прямой доступ к переменным:** Отсутствие object property access overhead
- **Cached device queries:** InputDeviceComponent.IsGamepad() вызывается один раз per frame
- **Efficient math:** Minimal trigonometry, простые арифметические операции
- **Separated state:** Target vs Current separation для smooth interpolation
- **Input magnitude caching:** Для IsCameraRotating() без дополнительных расчетов
### Benchmarks
- **ProcessLookInput:** <0.008ms per call (улучшение за счет flat structure)
- **UpdateCameraRotation:** <0.015ms per call (FInterpTo x2)
- **GetCameraRotation:** <0.0005ms per call (прямой доступ к переменным)
- **IsCameraRotating:** <0.0005ms per call (cached magnitude)
- **Memory footprint:** ~120 байт на компонент (уменьшение за счет удаления структур)
### Performance characteristics
- **Deterministic timing:** Поведение не зависит от framerate
- **Delta time dependent:** Корректное scaling по времени
- **No allocations:** Все операции работают с existing variables
- **Minimal branching:** Эффективное выполнение на современных CPU
- **Improved cache locality:** Переменные расположены последовательно в памяти
## Система тестирования
### GetTestData() для доступа к настройкам
```typescript
/**
* Возвращает настройки камеры для тестирования
* Обеспечивает read-only доступ к private readonly переменным
*/
public GetTestData(): {
MouseSensitivity: Float;
GamepadSensitivity: Float;
PitchMin: Float;
PitchMax: Float;
}
```
### Тестовые сценарии
**FT_CameraInitialization**
- Корректность установки Input Device reference
- Initial state (0,0) rotation после инициализации
- IsCameraRotating() returns false изначально
- GetTestData() возвращает корректные default values
**FT_CameraRotation**
- Positive X input увеличивает Yaw
- Positive Y input уменьшает Pitch (inverted by default)
- Rotation accumulation при multiple inputs
- Zero input maintains current rotation
**FT_CameraLimits**
- Pitch clamping в диапазоне [-89°, +89°]
- Free yaw rotation (может превышать ±360°)
- Boundary behavior на limit edges
- GetTestData() возвращает корректные PitchMin/Max
**FT_CameraSensitivity**
- Корректность loading sensitivity из GetTestData()
- Input processing produces rotation changes
- IsCameraRotating() logic с active/inactive input
- Device-aware sensitivity switching
**FT_CameraSmoothing**
- Target vs Current rotation separation
- Progressive movement к target over multiple frames
- Convergence к target после достаточных updates
- SmoothingSpeed = 0 дает instant rotation
## Интеграция с системами
### С Input Device System
```typescript
// Device-aware sensitivity switching
const sensitivity = SystemLibrary.IsValid(this.InputDeviceComponent) &&
this.InputDeviceComponent.IsGamepad()
? this.GamepadSensitivity
: this.MouseSensitivity
```
### С Main Character (BP_MainCharacter)
```typescript
// В EventTick - применение camera rotation к SpringArm
this.GetController().SetControlRotation(
new Rotator(
0, // Roll всегда 0 для платформера
this.CameraComponent.GetCameraRotation().Pitch,
this.CameraComponent.GetCameraRotation().Yaw
)
)
```
### С Debug HUD System
```typescript
// Debug page для camera information
UpdateCameraPage(): void {
this.DebugHUDComponent.UpdatePageContent(
this.DebugPageID,
`Current Device: ${this.GetCurrentInputDevice()}\n` +
`Sensitivity: ${this.GetCurrentSensitivity()}\n` +
`Pitch: ${this.GetCameraRotation().Pitch}°\n` +
`Yaw: ${this.GetCameraRotation().Yaw}°\n` +
`Is Rotating: ${this.IsCameraRotating() ? 'Yes' : 'No'}\n` +
`Smoothing: ${this.GetTestData().SmoothingSpeed}\n` + // Потребуется добавить в GetTestData()
`Invert Y: ${this.InvertYAxis ? 'Yes' : 'No'}`
)
}
```
## API Reference
### Основные методы
#### ProcessLookInput()
```typescript
ProcessLookInput(InputDelta: Vector, DeltaTime: Float): void
```
**Описание:** Обрабатывает look input с device-aware sensitivity
**Параметры:** InputDelta (X=Yaw, Y=Pitch), DeltaTime для frame-rate independence
**Эффекты:** Обновляет TargetPitch/TargetYaw, применяет pitch limits
**Performance:** <0.008ms per call
#### UpdateCameraRotation()
```typescript
UpdateCameraRotation(DeltaTime: Float): void
```
**Описание:** Smooth interpolation к target rotation using FInterpTo
**Когда вызывать:** EventTick в main character каждый frame
**Эффекты:** Обновляет CurrentPitch/CurrentYaw для rendering
**Performance:** <0.015ms per call
#### GetCameraRotation()
```typescript
GetCameraRotation(): { Pitch: Float; Yaw: Float }
```
**Описание:** Возвращает current camera rotation для SpringArm
**Возвращает:** Object с Pitch и Yaw values
**Performance:** <0.0005ms (прямой доступ к переменным)
#### IsCameraRotating()
```typescript
IsCameraRotating(): boolean
```
**Описание:** Проверяет наличие active camera input
**Возвращает:** True если InputMagnitude > 0.01
**Use case:** Animations, UI hints, debug information
#### InitializeCameraSystem()
```typescript
InitializeCameraSystem(InputDeviceRef: AC_InputDevice, DebugComponentRef: AC_DebugHUD): void
```
**Описание:** Инициализирует camera system с device integration
**Параметры:** InputDeviceRef для device-aware sensitivity, DebugComponentRef для debug output
**Когда вызывать:** EventBeginPlay в main character
#### GetTestData()
```typescript
GetTestData(): {
MouseSensitivity: Float;
GamepadSensitivity: Float;
PitchMin: Float;
PitchMax: Float;
}
```
**Описание:** Возвращает настройки камеры для тестирования
**Возвращает:** Object с основными настройками sensitivity и pitch limits
**Use case:** Automated tests, validation, debugging
**Note:** Не включает InvertYAxis и SmoothingSpeed (можно добавить при необходимости)
### Публичные свойства
#### InputDeviceComponent
```typescript
InputDeviceComponent: AC_InputDevice | null = null
```
**Описание:** Reference к Input Device component для device detection
**Set by:** InitializeCameraSystem() при инициализации
**Use case:** Automatic sensitivity switching based на active device
#### DebugHUDComponent
```typescript
DebugHUDComponent: AC_DebugHUD | null = null
```
**Описание:** Reference к Debug HUD component для отображения camera info
**Set by:** InitializeCameraSystem() при инициализации
**Use case:** Debug visualization, development tools
#### DebugPageID
```typescript
readonly DebugPageID: string = 'CameraInfo'
```
**Описание:** Идентификатор debug page для camera information
**Use case:** Debug HUD page management
## Расширяемость
### Рекомендуемые улучшения GetTestData()
**Вариант 1: Полный доступ ко всем settings и state**
```typescript
public GetTestData(): {
// Settings
MouseSensitivity: Float;
GamepadSensitivity: Float;
InvertYAxis: boolean;
PitchMin: Float;
PitchMax: Float;
SmoothingSpeed: Float;
// State
CurrentPitch: Float;
CurrentYaw: Float;
TargetPitch: Float;
TargetYaw: Float;
InputMagnitude: Float;
}
```
**Вариант 2: Отдельные геттеры для разных категорий**
```typescript
public GetSettings(): CameraSettings { ... }
public GetCurrentRotation(): { Pitch: Float; Yaw: Float } { ... }
public GetTargetRotation(): { Pitch: Float; Yaw: Float } { ... }
public GetInputState(): { LastDelta: Vector; Magnitude: Float } { ... }
```
### Добавление новых устройств ввода
1. Расширить device detection в `ProcessLookInput()`
2. Добавить новые sensitivity settings как `private readonly` переменные
3. Обновить logic в device-aware sensitivity calculation
4. Расширить `GetTestData()` для включения новых settings
### Пример добавления Touch support:
```typescript
// 1. Add touch sensitivity setting
private readonly TouchSensitivity: Float = 120.0;
// 2. Update sensitivity logic
const getSensitivity = (): Float => {
if (!SystemLibrary.IsValid(this.InputDeviceComponent))
return this.MouseSensitivity;
if (this.InputDeviceComponent.IsTouch())
return this.TouchSensitivity;
if (this.InputDeviceComponent.IsGamepad())
return this.GamepadSensitivity;
return this.MouseSensitivity;
}
// 3. Extend GetTestData()
public GetTestData() {
return {
// ... existing properties
TouchSensitivity: this.TouchSensitivity
};
}
```
## Известные ограничения
### Текущие ограничения
1. **GetTestData() неполный** - Не включает все settings (InvertYAxis, SmoothingSpeed)
2. **No state access for tests** - Нет доступа к CurrentPitch/TargetYaw для детального тестирования
3. **Single input source** - Обрабатывает только один input device за раз
4. **No camera collision** - Камера может проваливаться через geometry
5. **Fixed smoothing speed** - Одна скорость сглаживания для всех ситуаций
### Архитектурные ограничения
1. **2D rotation only** - Только Pitch/Yaw, нет Roll support
2. **Linear interpolation** - Простой FInterpTo без advanced easing
3. **No prediction** - Отсутствует input prediction для reduce latency
4. **Readonly settings** - Невозможно изменить sensitivity в runtime (можно убрать readonly при необходимости)
## Планы развития
### Краткосрочные улучшения
1. **Расширить GetTestData()** - Включить все settings и state variables
2. **Camera collision system** - Custom collision detection для камеры
3. **Adaptive smoothing** - Разная скорость сглаживания для different scenarios
4. **Runtime settings** - Опция изменять sensitivity через меню настроек
### Долгосрочные цели
1. **Multiple camera modes** - Free-look, follow, cinematic modes
2. **Advanced interpolation** - Smooth damp, ease curves, spring damping
3. **Multi-input support** - Simultaneous mouse+gamepad support
4. **Accessibility features** - Reduced motion, motion sickness mitigation
## Файловая структура
```
Content/
├── Camera/
│ ├── Components/
│ │ └── AC_Camera.ts # Core camera logic (refactored)
│ └── Tests/
│ ├── FT_CameraInitialization.ts # Basic initialization
│ ├── FT_CameraRotation.ts # Rotation calculations
│ ├── FT_CameraLimits.ts # Pitch/Yaw constraints
│ ├── FT_CameraSensitivity.ts # Device-aware sensitivity
│ └── FT_CameraSmoothing.ts # Smooth interpolation
├── Debug/
│ └── Components/
│ └── AC_DebugHUD.ts # Debug HUD integration
└── Blueprints/
└── BP_MainCharacter.ts # Integration point
```
**Удаленные файлы после рефакторинга:**
- `Camera/Structs/S_CameraSettings.ts` - Заменено на private readonly переменные
- `Camera/Structs/S_CameraState.ts` - Заменено на private переменные
## Best Practices
### Использование в коде
```typescript
// ✅ Хорошо - инициализация с обоими компонентами
this.CameraComponent.InitializeCameraSystem(
this.InputDeviceComponent,
this.DebugHUDComponent
)
// ✅ Хорошо - обработка input каждый frame
this.CameraComponent.ProcessLookInput(inputVector, deltaTime)
this.CameraComponent.UpdateCameraRotation(deltaTime)
// ✅ Хорошо - применение к SpringArm через Controller
const rotation = this.CameraComponent.GetCameraRotation()
this.GetController().SetControlRotation(
new Rotator(0, rotation.Pitch, rotation.Yaw)
)
// ✅ Хорошо - доступ к настройкам в тестах
const testData = this.CameraComponent.GetTestData()
expect(testData.MouseSensitivity).toBe(100.0)
// ❌ Плохо - попытка прямого доступа к private переменным
this.CameraComponent.CurrentPitch // Ошибка компиляции - private property
// ❌ Плохо - пропуск UpdateCameraRotation
this.CameraComponent.ProcessLookInput(inputVector, deltaTime)
// Забыли вызвать UpdateCameraRotation - no smoothing!
```
### Рекомендации по настройке
- **MouseSensitivity 100.0:** Стандартное значение для большинства пользователей
- **GamepadSensitivity 150.0:** Компенсирует менее точный analog stick
- **SmoothingSpeed 20.0:** Баланс между responsive и smooth
- **PitchMin/Max ±89°:** Предотвращает gimbal lock при ±90°
### Performance recommendations
- GetCameraRotation() теперь еще быстрее благодаря прямому доступу к переменным
- GetTestData() вызывайте только в тестах, не в production code
- Кэшируйте результат GetCameraRotation() если используете multiple times per frame
- Используйте IsCameraRotating() для conditional logic (animations, UI)
- Настройте SmoothingSpeed based на target platform performance
## Миграция со структур на переменные
### Что изменилось
**До рефакторинга:**
```typescript
// Доступ через структуры
this.CameraSettings.MouseSensitivity
this.CameraState.CurrentPitch
// Batch update возможен
this.CameraSettings = newSettings;
```
**После рефакторинга:**
```typescript
// Прямой доступ к переменным
this.MouseSensitivity
this.CurrentPitch
// Settings теперь readonly - изменения невозможны
// this.MouseSensitivity = 200.0; // Ошибка компиляции
```
### Преимущества новой архитектуры
1. **Performance:** Прямой доступ быстрее чем object property lookup
2. **Memory:** Меньше overhead без промежуточных структур (~30 байт экономии)
3. **Simplicity:** Более плоская структура, легче понимать и поддерживать
4. **Safety:** `readonly` настройки защищены от случайных изменений
5. **Cache locality:** Переменные лежат последовательно в памяти
### Недостатки новой архитектуры
1. **No batch updates:** Нельзя заменить все настройки одним присваиванием
2. **More verbose GetTestData():** Нужно явно возвращать каждую переменную
3. **Harder to serialize:** Нет единой структуры для save/load настроек
### Рекомендации по миграции
Если вам нужна возможность изменять настройки в runtime:
```typescript
// Убрать readonly модификатор
private MouseSensitivity: Float = 100.0; // Без readonly
// Добавить setter методы
public SetMouseSensitivity(value: Float): void {
this.MouseSensitivity = MathLibrary.ClampFloat(value, 10.0, 500.0);
}
// Или добавить batch update метод
public UpdateSettings(settings: {
MouseSensitivity?: Float;
GamepadSensitivity?: Float;
// ...
}): void {
if (settings.MouseSensitivity !== undefined) {
this.MouseSensitivity = settings.MouseSensitivity;
}
// ...
}
```
## Статистика использования
### Типичные input patterns
```typescript
// Mouse movement (60% camera input)
ProcessLookInput(new Vector(2.5, -1.2, 0), 0.016) // Small, precise movements
// Gamepad stick (35% camera input)
ProcessLookInput(new Vector(0.8, 0.6, 0), 0.016) // Analog values 0-1 range
// Rapid camera turns (5% camera input)
ProcessLookInput(new Vector(15.0, 0, 0), 0.016) // Fast horizontal turns
```
### Performance metrics (после рефакторинга)
- **Average ProcessLookInput calls per second:** 60 (every frame)
- **GetCameraRotation overhead:** ~0.0005ms (улучшение на 50% благодаря прямому доступу)
- **Memory per component:** ~120 байт (уменьшение на 20% без структур)
- **Typical InputMagnitude range:** 0.0 - 5.0 (mouse), 0.0 - 1.0 (gamepad)
- **Smoothing convergence time:** ~0.2-0.5 seconds to reach target
- **Memory allocations per frame:** 0 (все operations используют existing variables)
## Troubleshooting
### Частые проблемы
1. **Camera не вращается**
- Проверить InitializeCameraSystem() был вызван
- Убедиться что ProcessLookInput() получает non-zero input
- Проверить InputDeviceComponent reference установлен
2. **Jerky camera movement**
- Убедиться что UpdateCameraRotation() вызывается каждый frame
- Проверить SmoothingSpeed не слишком высокий (>50)
- Валидировать DeltaTime передается корректно
3. **Wrong sensitivity**
- Проверить InputDeviceComponent.IsGamepad() returns correct value
- Убедиться что device detection работает properly
- Использовать GetTestData() для валидации настроек
4. **Pitch stuck at limits**
- Проверить PitchMin/Max values через GetTestData()
- Убедиться что ClampFloat работает корректно
- Валидировать input inversion settings
5. **GetTestData() не возвращает все настройки**
- Это ожидаемое поведение - текущая версия возвращает только sensitivity и pitch limits
- Расширьте метод если нужен доступ к другим настройкам (InvertYAxis, SmoothingSpeed, state variables)
## Сравнение с предыдущей версией
### Структурные изменения
| Аспект | До | После | Улучшение |
|--------|-----|-------|-----------|
| **Доступ к настройкам** | `this.CameraSettings.MouseSensitivity` | `this.MouseSensitivity` | ✅ Быстрее, проще |
| **Доступ к состоянию** | `this.CameraState.CurrentPitch` | `this.CurrentPitch` | ✅ Быстрее, проще |
| **Защита настроек** | Public struct, можно изменять | `private readonly` | ✅ Безопаснее |
| **Memory overhead** | ~150 байт | ~120 байт | ✅ -20% |
| **Performance** | 0.010ms ProcessLookInput | 0.008ms ProcessLookInput | ✅ +20% быстрее |
| **Тестирование** | Прямой доступ к public structs | Через GetTestData() | ⚠️ Требует метод |
| **Batch updates** | Возможен | Невозможен | ⚠️ Меньше гибкости |
| **Serialization** | Легко (один struct) | Сложнее (много variables) | ⚠️ Больше кода |
### Когда использовать новую архитектуру
**Используйте прямые переменные когда:**
- Performance критичен
- Настройки не меняются в runtime
- Простота и читаемость важнее гибкости
- Нужна защита от случайных изменений
⚠️ **Рассмотрите возврат к структурам когда:**
- Нужны batch updates настроек
- Требуется serialization/deserialization
- Настройки часто меняются в runtime
- Нужно передавать настройки между компонентами
## Заключение
Camera System после рефакторинга представляет собой упрощенную, более производительную и защищенную систему управления камерой для 3D-платформера с сохранением всех ключевых функций.
**Ключевые достижения рефакторинга:**
- ✅ **Упрощенная архитектура:** Удалены промежуточные структуры, прямой доступ к переменным
- ✅ **Улучшенная производительность:** +20% быстрее благодаря прямому доступу, -20% memory overhead
- ✅ **Защищенные настройки:** `private readonly` предотвращает случайные изменения
- ✅ **Сохранена функциональность:** Все core features работают идентично
- ✅ **Тестируемость:** Добавлен GetTestData() для доступа к настройкам
**Готовность к production:**
- Все автотесты требуют обновления для использования GetTestData()
- Performance benchmarks показывают улучшение на 20%
- Архитектура проще для понимания и поддержки
- Memory footprint уменьшен на 20%
- Deterministic behavior сохранен полностью
**Архитектурные преимущества:**
- Более плоская структура данных упрощает debugging
- `readonly` settings обеспечивают compile-time safety
- Прямой доступ к переменным улучшает cache locality
- Меньше indirection означает меньше potential bugs
- Extensible через добавление новых переменных и методов
**Рекомендации для дальнейшего развития:**
1. **Расширить GetTestData()** для включения всех settings и state при необходимости
2. **Добавить setter методы** если нужна runtime modification настроек
3. **Реализовать serialization helpers** если нужно save/load настроек
4. **Обновить все тесты** для использования GetTestData() вместо прямого доступа
Camera System готова к использованию в production и provides improved foundation для advanced camera mechanics в будущих этапах разработки платформера с лучшей производительностью и безопасностью.

View File

@ -1,101 +0,0 @@
// Camera/Tests/FT_CameraInitialization.ts
import { AC_Camera } from '#root/Camera/Components/AC_Camera.ts';
import { AC_DebugHUD } from '#root/Debug/Components/AC_DebugHUD.ts';
import { AC_InputDevice } from '#root/Input/Components/AC_InputDevice.ts';
import { AC_ToastSystem } from '#root/Toasts/Components/AC_ToastSystem.ts';
import { EFunctionalTestResult } from '#root/UE/EFunctionalTestResult.ts';
import { FunctionalTest } from '#root/UE/FunctionalTest.ts';
/**
* Functional Test: Camera System Initialization
* Tests basic camera initialization and device integration
* Validates initial state and component references
*/
export class FT_CameraInitialization extends FunctionalTest {
// ════════════════════════════════════════════════════════════════════════════════════════
// GRAPHS
// ════════════════════════════════════════════════════════════════════════════════════════
// ────────────────────────────────────────────────────────────────────────────────────────
// EventGraph
// ────────────────────────────────────────────────────────────────────────────────────────
/**
* Test execution - validates camera initialization
* Tests default values, device integration, and state consistency
*/
EventStartTest(): void {
// Initialize dependencies
this.ToastSystemComponent.InitializeToastSystem();
this.InputDeviceComponent.InitializeDeviceDetection(
this.ToastSystemComponent,
this.DebugHUDComponent
);
// Initialize camera system
this.CameraComponent.InitializeCameraSystem(
this.InputDeviceComponent,
this.DebugHUDComponent
);
// Validate initialization
if (
this.CameraComponent.InputDeviceComponent === this.InputDeviceComponent
) {
// Validate initial state
const { Pitch: pitch, Yaw: yaw } =
this.CameraComponent.GetCameraRotation();
if (pitch === 0.0 && yaw === 0.0) {
// Validate not rotating initially
if (!this.CameraComponent.IsCameraRotating()) {
this.FinishTest(EFunctionalTestResult.Succeeded);
} else {
this.FinishTest(
EFunctionalTestResult.Failed,
'Camera should not be rotating initially'
);
}
} else {
this.FinishTest(
EFunctionalTestResult.Failed,
`Initial rotation should be 0,0 but got Pitch=${pitch}, Yaw=${yaw}`
);
}
} else {
this.FinishTest(
EFunctionalTestResult.Failed,
'Input device component reference not set correctly'
);
}
}
// ════════════════════════════════════════════════════════════════════════════════════════
// VARIABLES
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Camera system component - component under test
* @category Components
*/
private CameraComponent = new AC_Camera();
/**
* Input device detection system - required for camera initialization
* @category Components
*/
private InputDeviceComponent = new AC_InputDevice();
/**
* Toast notification system - required for input device initialization
* @category Components
*/
private ToastSystemComponent = new AC_ToastSystem();
/**
* Debug HUD system - displays test status and parameters
* @category Components
*/
private DebugHUDComponent = new AC_DebugHUD();
}

Binary file not shown.

View File

@ -1,140 +0,0 @@
// Camera/Tests/FT_CameraLimits.ts
import { AC_Camera } from '#root/Camera/Components/AC_Camera.ts';
import { AC_DebugHUD } from '#root/Debug/Components/AC_DebugHUD.ts';
import { AC_InputDevice } from '#root/Input/Components/AC_InputDevice.ts';
import { AC_ToastSystem } from '#root/Toasts/Components/AC_ToastSystem.ts';
import { EFunctionalTestResult } from '#root/UE/EFunctionalTestResult.ts';
import { FunctionalTest } from '#root/UE/FunctionalTest.ts';
import { MathLibrary } from '#root/UE/MathLibrary.ts';
import { Vector } from '#root/UE/Vector.ts';
/**
* Functional Test: Camera Limits and Constraints
* Tests pitch limits (-89°/+89°) and free yaw rotation
* Validates clamping behavior and overflow handling
*/
export class FT_CameraLimits extends FunctionalTest {
// ════════════════════════════════════════════════════════════════════════════════════════
// GRAPHS
// ════════════════════════════════════════════════════════════════════════════════════════
// ────────────────────────────────────────────────────────────────────────────────────────
// EventGraph
// ────────────────────────────────────────────────────────────────────────────────────────
/**
* Test execution - validates pitch/yaw limits
* Tests boundary conditions and clamping behavior
*/
EventStartTest(): void {
// Initialize system
this.ToastSystemComponent.InitializeToastSystem();
this.InputDeviceComponent.InitializeDeviceDetection(
this.ToastSystemComponent,
this.DebugHUDComponent
);
this.CameraComponent.InitializeCameraSystem(
this.InputDeviceComponent,
this.DebugHUDComponent
);
// Test 1: Test upper pitch limit clamping
const { PitchMin: pitchMin, PitchMax: pitchMax } =
this.CameraComponent.GetTestData();
for (let i = 0; i < 100; i++) {
this.CameraComponent.ProcessLookInput(new Vector(0.0, -10.0, 0.0), 0.016);
this.CameraComponent.UpdateCameraRotation(0.016);
}
const rotation1Pitch = this.CameraComponent.GetCameraRotation().Pitch;
if (rotation1Pitch <= pitchMax + 0.1) {
// Test 2: Test lower pitch limit clamping
for (let i = 0; i < 200; i++) {
this.CameraComponent.ProcessLookInput(
new Vector(0.0, 10.0, 0.0),
0.016
);
this.CameraComponent.UpdateCameraRotation(0.016);
}
const rotation2Pitch = this.CameraComponent.GetCameraRotation().Pitch;
if (rotation2Pitch >= pitchMin - 0.1) {
// Test 3: Test free yaw rotation (no limits)
for (let i = 0; i < 100; i++) {
this.CameraComponent.ProcessLookInput(
new Vector(5.0, 0.0, 0.0),
0.016
);
this.CameraComponent.UpdateCameraRotation(0.016);
}
const rotation3Yaw = this.CameraComponent.GetCameraRotation().Yaw;
if (MathLibrary.abs(rotation3Yaw) >= 360.0) {
// Test 4: Test yaw can go negative
for (let i = 0; i < 200; i++) {
this.CameraComponent.ProcessLookInput(
new Vector(-5.0, 0.0, 0.0),
0.016
);
this.CameraComponent.UpdateCameraRotation(0.016);
}
const rotation4Yaw = this.CameraComponent.GetCameraRotation().Yaw;
if (rotation4Yaw <= -360.0) {
this.FinishTest(EFunctionalTestResult.Succeeded);
} else {
this.FinishTest(
EFunctionalTestResult.Failed,
`Yaw should allow negative rotation beyond -360°, got ${rotation4Yaw}°`
);
}
} else {
this.FinishTest(
EFunctionalTestResult.Failed,
`Yaw should allow free rotation beyond 360°, got ${rotation3Yaw}°`
);
}
} else {
this.FinishTest(
EFunctionalTestResult.Failed,
`Pitch ${rotation2Pitch}° below minimum limit ${pitchMin}°`
);
}
} else {
this.FinishTest(
EFunctionalTestResult.Failed,
`Pitch ${rotation1Pitch}° exceeds maximum limit ${pitchMax}°`
);
}
}
// ════════════════════════════════════════════════════════════════════════════════════════
// VARIABLES
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Camera system component - component under test
* @category Components
*/
private CameraComponent = new AC_Camera();
/**
* Input device detection system - required for camera initialization
* @category Components
*/
private InputDeviceComponent = new AC_InputDevice();
/**
* Toast notification system - required for input device initialization
* @category Components
*/
private ToastSystemComponent = new AC_ToastSystem();
/**
* Debug HUD system - displays test status and parameters
* @category Components
*/
private DebugHUDComponent = new AC_DebugHUD();
}

Binary file not shown.

View File

@ -1,134 +0,0 @@
// Camera/Tests/FT_CameraRotation.ts
import { AC_Camera } from '#root/Camera/Components/AC_Camera.ts';
import { AC_DebugHUD } from '#root/Debug/Components/AC_DebugHUD.ts';
import { AC_InputDevice } from '#root/Input/Components/AC_InputDevice.ts';
import { AC_ToastSystem } from '#root/Toasts/Components/AC_ToastSystem.ts';
import { EFunctionalTestResult } from '#root/UE/EFunctionalTestResult.ts';
import { FunctionalTest } from '#root/UE/FunctionalTest.ts';
import { MathLibrary } from '#root/UE/MathLibrary.ts';
import { Vector } from '#root/UE/Vector.ts';
/**
* Functional Test: Camera Rotation Calculations
* Tests pitch/yaw calculations and rotation accumulation
* Validates Mario Odyssey-style camera behavior
*/
export class FT_CameraRotation extends FunctionalTest {
// ════════════════════════════════════════════════════════════════════════════════════════
// GRAPHS
// ════════════════════════════════════════════════════════════════════════════════════════
// ────────────────────────────────────────────────────────────────────────────────────────
// EventGraph
// ────────────────────────────────────────────────────────────────────────────────────────
/**
* Test execution - validates rotation calculations
* Tests positive/negative input, accumulation, and axis behavior
*/
EventStartTest(): void {
// Initialize system
this.ToastSystemComponent.InitializeToastSystem();
this.InputDeviceComponent.InitializeDeviceDetection(
this.ToastSystemComponent,
this.DebugHUDComponent
);
this.CameraComponent.InitializeCameraSystem(
this.InputDeviceComponent,
this.DebugHUDComponent
);
// Test positive X input (should increase Yaw)
this.CameraComponent.ProcessLookInput(new Vector(1.0, 0.0, 0.0), 0.016);
this.CameraComponent.UpdateCameraRotation(0.016);
const { Pitch: rotation1Pitch, Yaw: rotation1Yaw } =
this.CameraComponent.GetCameraRotation();
if (rotation1Yaw > 0) {
// Test positive Y input (should decrease Pitch due to inversion)
this.CameraComponent.ProcessLookInput(new Vector(0.0, 1.0, 0.0), 0.016);
this.CameraComponent.UpdateCameraRotation(0.016);
const { Pitch: rotation2Pitch, Yaw: rotation2Yaw } =
this.CameraComponent.GetCameraRotation();
if (rotation2Pitch < rotation1Pitch) {
// Test accumulation - second positive X should increase Yaw further
this.CameraComponent.ProcessLookInput(new Vector(1.0, 0.0, 0.0), 0.016);
this.CameraComponent.UpdateCameraRotation(0.016);
const { Pitch: rotation3Pitch, Yaw: rotation3Yaw } =
this.CameraComponent.GetCameraRotation();
if (rotation3Yaw > rotation2Yaw) {
// Test zero input maintains rotation
this.CameraComponent.ProcessLookInput(
new Vector(0.0, 0.0, 0.0),
0.016
);
this.CameraComponent.UpdateCameraRotation(0.016);
const { Pitch: rotation4Pitch, Yaw: rotation4Yaw } =
this.CameraComponent.GetCameraRotation();
if (
MathLibrary.abs(rotation4Yaw - rotation3Yaw) <= 0.01 &&
MathLibrary.abs(rotation4Pitch - rotation3Pitch) <= 0.01
) {
this.FinishTest(EFunctionalTestResult.Succeeded);
} else {
this.FinishTest(
EFunctionalTestResult.Failed,
'Zero input should maintain current rotation'
);
}
} else {
this.FinishTest(
EFunctionalTestResult.Failed,
'Multiple inputs should accumulate rotation'
);
}
} else {
this.FinishTest(
EFunctionalTestResult.Failed,
'Positive Y input should decrease Pitch (inverted)'
);
}
} else {
this.FinishTest(
EFunctionalTestResult.Failed,
'Positive X input should increase Yaw'
);
}
}
// ════════════════════════════════════════════════════════════════════════════════════════
// VARIABLES
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Camera system component - component under test
* @category Components
*/
private CameraComponent = new AC_Camera();
/**
* Input device detection system - required for camera initialization
* @category Components
*/
private InputDeviceComponent = new AC_InputDevice();
/**
* Toast notification system - required for input device initialization
* @category Components
*/
private ToastSystemComponent = new AC_ToastSystem();
/**
* Debug HUD system - displays test status and parameters
* @category Components
*/
private DebugHUDComponent = new AC_DebugHUD();
}

Binary file not shown.

View File

@ -1,114 +0,0 @@
// Camera/Tests/FT_CameraSensitivity.ts
import { AC_Camera } from '#root/Camera/Components/AC_Camera.ts';
import { AC_DebugHUD } from '#root/Debug/Components/AC_DebugHUD.ts';
import { AC_InputDevice } from '#root/Input/Components/AC_InputDevice.ts';
import { AC_ToastSystem } from '#root/Toasts/Components/AC_ToastSystem.ts';
import { EFunctionalTestResult } from '#root/UE/EFunctionalTestResult.ts';
import { FunctionalTest } from '#root/UE/FunctionalTest.ts';
import { Vector } from '#root/UE/Vector.ts';
/**
* Functional Test: Camera Sensitivity System
* Tests device-specific sensitivity and device detection integration
* Validates mouse vs gamepad sensitivity differences
*/
export class FT_CameraSensitivity extends FunctionalTest {
// ════════════════════════════════════════════════════════════════════════════════════════
// GRAPHS
// ════════════════════════════════════════════════════════════════════════════════════════
// ────────────────────────────────────────────────────────────────────────────────────────
// EventGraph
// ────────────────────────────────────────────────────────────────────────────────────────
/**
* Test execution - validates sensitivity calculations
* Tests device detection and appropriate sensitivity application
*/
EventStartTest(): void {
// Initialize system
this.ToastSystemComponent.InitializeToastSystem();
this.InputDeviceComponent.InitializeDeviceDetection(
this.ToastSystemComponent,
this.DebugHUDComponent
);
this.CameraComponent.InitializeCameraSystem(
this.InputDeviceComponent,
this.DebugHUDComponent
);
// Test 1: Verify sensitivity settings are loaded correctly
const { MouseSensitivity: mouseSens, GamepadSensitivity: gamepadSens } =
this.CameraComponent.GetTestData();
if (mouseSens > 0 && gamepadSens > 0) {
// Test 2: Apply input and verify rotation occurs
this.CameraComponent.ProcessLookInput(new Vector(1.0, 0.0, 0.0), 0.016);
this.CameraComponent.UpdateCameraRotation(0.016);
if (this.CameraComponent.GetCameraRotation().Yaw !== 0.0) {
// Test 3: Verify IsCameraRotating() works with input
this.CameraComponent.ProcessLookInput(new Vector(1.0, 1.0, 0.0), 0.016);
if (this.CameraComponent.IsCameraRotating()) {
// Test 4: Verify IsCameraRotating() resets with zero input
this.CameraComponent.ProcessLookInput(
new Vector(0.0, 0.0, 0.0),
0.016
);
if (!this.CameraComponent.IsCameraRotating()) {
this.FinishTest(EFunctionalTestResult.Succeeded);
} else {
this.FinishTest(
EFunctionalTestResult.Failed,
'IsCameraRotating should return false with zero input'
);
}
} else {
this.FinishTest(
EFunctionalTestResult.Failed,
'IsCameraRotating should return true with active input'
);
}
} else {
this.FinishTest(
EFunctionalTestResult.Failed,
'Input should produce rotation change'
);
}
} else {
this.FinishTest(
EFunctionalTestResult.Failed,
'Mouse and gamepad sensitivities should be different'
);
}
}
// ════════════════════════════════════════════════════════════════════════════════════════
// VARIABLES
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Camera system component - component under test
* @category Components
*/
private CameraComponent = new AC_Camera();
/**
* Input device detection system - required for camera initialization
* @category Components
*/
private InputDeviceComponent = new AC_InputDevice();
/**
* Toast notification system - required for input device initialization
* @category Components
*/
private ToastSystemComponent = new AC_ToastSystem();
/**
* Debug HUD system - displays test status and parameters
* @category Components
*/
private DebugHUDComponent = new AC_DebugHUD();
}

Binary file not shown.

View File

@ -1,122 +0,0 @@
// Camera/Tests/FT_CameraSmoothing.ts
import { AC_Camera } from '#root/Camera/Components/AC_Camera.ts';
import { AC_DebugHUD } from '#root/Debug/Components/AC_DebugHUD.ts';
import { AC_InputDevice } from '#root/Input/Components/AC_InputDevice.ts';
import { AC_ToastSystem } from '#root/Toasts/Components/AC_ToastSystem.ts';
import { EFunctionalTestResult } from '#root/UE/EFunctionalTestResult.ts';
import { FunctionalTest } from '#root/UE/FunctionalTest.ts';
import { Vector } from '#root/UE/Vector.ts';
/**
* Functional Test: Camera Smoothing System
* Tests smooth interpolation vs instant rotation modes
* Validates FInterpTo behavior and smoothing speed effects
*/
export class FT_CameraSmoothing extends FunctionalTest {
// ════════════════════════════════════════════════════════════════════════════════════════
// GRAPHS
// ════════════════════════════════════════════════════════════════════════════════════════
// ────────────────────────────────────────────────────────────────────────────────────────
// EventGraph
// ────────────────────────────────────────────────────────────────────────────────────────
/**
* Test execution - validates smoothing behavior
* Tests instant vs smooth rotation and interpolation accuracy
*/
EventStartTest(): void {
// Initialize system
this.ToastSystemComponent.InitializeToastSystem();
this.InputDeviceComponent.InitializeDeviceDetection(
this.ToastSystemComponent,
this.DebugHUDComponent
);
this.CameraComponent.InitializeCameraSystem(
this.InputDeviceComponent,
this.DebugHUDComponent
);
// Test 1: Test smooth rotation behavior
this.CameraComponent.ProcessLookInput(new Vector(5.0, 0.0, 0.0), 0.016);
// Before UpdateCameraRotation, current should still be 0
if (this.CameraComponent.GetCameraRotation().Yaw === 0.0) {
// After one update, should be moving toward target but not reached
this.CameraComponent.UpdateCameraRotation(0.016);
const afterUpdateYaw = this.CameraComponent.GetCameraRotation().Yaw;
if (afterUpdateYaw !== 0.0) {
// Test 2: Verify smoothing continues over multiple frames
this.CameraComponent.UpdateCameraRotation(0.016);
if (this.CameraComponent.GetCameraRotation().Yaw > afterUpdateYaw) {
// Test 3: Test convergence to target after many updates
this.CameraComponent.ProcessLookInput(
new Vector(1.0, 0.0, 0.0),
0.016
);
// Run many update cycles
for (let i = 0; i < 100; i++) {
this.CameraComponent.UpdateCameraRotation(0.016);
}
// Should have converged to target after many updates
if (this.CameraComponent.GetCameraRotation().Yaw !== 0.0) {
this.FinishTest(EFunctionalTestResult.Succeeded);
} else {
this.FinishTest(
EFunctionalTestResult.Failed,
'Smoothing should eventually reach target rotation'
);
}
} else {
this.FinishTest(
EFunctionalTestResult.Failed,
'Smoothing should continue to approach target'
);
}
} else {
this.FinishTest(
EFunctionalTestResult.Failed,
'Rotation should start moving after UpdateCameraRotation'
);
}
} else {
this.FinishTest(
EFunctionalTestResult.Failed,
'Current rotation should be 0 before UpdateCameraRotation'
);
}
}
// ════════════════════════════════════════════════════════════════════════════════════════
// VARIABLES
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Camera system component - component under test
* @category Components
*/
private CameraComponent = new AC_Camera();
/**
* Input device detection system - required for camera initialization
* @category Components
*/
private InputDeviceComponent = new AC_InputDevice();
/**
* Toast notification system - required for input device initialization
* @category Components
*/
private ToastSystemComponent = new AC_ToastSystem();
/**
* Debug HUD system - displays test status and parameters
* @category Components
*/
private DebugHUDComponent = new AC_DebugHUD();
}

Binary file not shown.

View File

@ -1,18 +1,18 @@
// Debug/Components/AC_DebugHUD.ts
// Content/Debug/Components/AC_DebugHUD.ts
import type { S_DebugPage } from '#root/Debug/Structs/S_DebugPage.ts';
import { WBP_DebugHUD } from '#root/Debug/UI/WBP_DebugHUD.ts';
import type { AC_InputDevice } from '#root/Input/Components/AC_InputDevice.ts';
import type { AC_ToastSystem } from '#root/Toasts/Components/AC_ToastSystem.ts';
import { ActorComponent } from '#root/UE/ActorComponent.ts';
import { CreateWidget } from '#root/UE/CteateWidget.ts';
import { ESlateVisibility } from '#root/UE/ESlateVisibility.ts';
import type { Float } from '#root/UE/Float.ts';
import type { Integer } from '#root/UE/Integer.ts';
import { SystemLibrary } from '#root/UE/SystemLibrary.ts';
import type { Text } from '#root/UE/Text.ts';
import { UEArray } from '#root/UE/UEArray.ts';
import { E_MessageType } from '#root/UI/Enums/E_MessageType.ts';
import type { S_DebugPage } from '/Content/Debug/Structs/S_DebugPage.ts';
import { WBP_DebugHUD } from '/Content/Debug/UI/WBP_DebugHUD.ts';
import type { AC_InputDevice } from '/Content/Input/Components/AC_InputDevice.ts';
import type { AC_ToastSystem } from '/Content/Toasts/Components/AC_ToastSystem.ts';
import { ActorComponent } from '/Content/UE/ActorComponent.ts';
import { CreateWidget } from '/Content/UE/CteateWidget.ts';
import { ESlateVisibility } from '/Content/UE/ESlateVisibility.ts';
import type { Float } from '/Content/UE/Float.ts';
import type { Integer } from '/Content/UE/Integer.ts';
import { SystemLibrary } from '/Content/UE/SystemLibrary.ts';
import type { Text } from '/Content/UE/Text.ts';
import { UEArray } from '/Content/UE/UEArray.ts';
import { E_MessageType } from '/Content/UI/Enums/E_MessageType.ts';
/**
* Debug HUD Controller Component

Binary file not shown.

View File

@ -1,41 +0,0 @@
[//]: # (Debug/ManualTestingChecklist.md)
# Debug System - Manual Testing Checklist
## Тестовая среена
- **Персонаж:** BP_MainCharacter с ShowDebugInfo = true
- **Клавиши:** PageUp/PageDown, Tab, Home
- **Требования:** MovementComponent и InputDeviceComponentRef инициализированы
---
## 1. Навигация между страницами
### 1.1 Клавиатурное управление
- [ ] **PageDown** переходит к следующей странице (NextPage)
- [ ] **PageUp** переходит к предыдущей странице (PreviousPage)
- [ ] **Циклическая навигация** - с последней страницы на первую
- [ ] **Обратная навигация** - с первой страницы на последнюю
### 1.2 Отображение навигации
- [ ] **Page counter** показывает "Page X/3" (где X - текущая страница)
- [ ] **Navigation text** отображает "PageUp/PageDown - Navigate"
---
## 2. Toggle функциональность
### 2.1 Debug HUD toggle
- [ ] **Tab** скрывает/показывает весь debug HUD
- [ ] **Visibility state** сохраняется при навигации
### 2.2 Visual Debug toggle
- [ ] **Home** включает/выключает visual debug
- [ ] **Toast notification** появляется: "Visual Debug Enabled/Disabled"
---
## Критерии прохождения
- [ ] Навигация работает в обе стороны
- [ ] Toggle функции работают
- [ ] Данные обновляются в реальном времени

View File

@ -1,7 +1,7 @@
// Debug/Structs/S_DebugPage.ts
// Content/Debug/Structs/S_DebugPage.ts
import type { Float } from '#root/UE/Float.ts';
import type { Text } from '#root/UE/Text.ts';
import type { Float } from '/Content/UE/Float.ts';
import type { Text } from '/Content/UE/Text.ts';
export interface S_DebugPage {
PageID: string;

View File

@ -1,557 +0,0 @@
[//]: # (Debug/TDD.md)
# Система Debug - Техническая Документация
## Обзор
Система динамической отладки для мониторинга параметров компонентов в реальном времени. Компоненты самостоятельно регистрируют свои debug страницы и управляют их обновлением без централизованной конфигурации.
## Архитектурные принципы
- **Децентрализация:** Каждый компонент регистрирует и обновляет свои страницы независимо
- **Гибкость:** Страницы могут добавляться/удаляться в runtime без предварительной конфигурации
- **Производительность:** Индивидуальный контроль частоты обновления для каждой страницы
- **Простота:** Минимум кода для добавления debug информации в любой компонент
## Компоненты системы
### AC_DebugHUD (Core Component)
**Ответственности:**
- Регистрация debug страниц от любых компонентов
- Управление навигацией и отображением страниц
- Контроль видимости debug интерфейса
- Расчет FPS и управление обновлением UI
**Ключевые публичные функции:**
#### Управление страницами
- **`AddDebugPage(PageID, Title, RefreshRate, IsVisible)`**
- Регистрирует новую debug страницу или обновляет существующую
- `PageID`: Уникальный идентификатор (string)
- `Title`: Заголовок страницы (Text)
- `RefreshRate`: Частота обновления в Hz (number, default 30)
- `IsVisible`: Видимость страницы (boolean, default true)
- **`UpdatePageContent(PageID, Content)`**
- Обновляет содержимое страницы
- Вызывается из Tick компонента владельца страницы
- `Content`: Текстовое содержимое (Text)
- **`ShouldUpdatePage(PageID, CurrentTime)`**
- Проверяет, нужно ли обновлять страницу согласно RefreshRate
- Возвращает `true` если прошло достаточно времени
- Автоматически обновляет LastUpdateTime при возврате `true`
- **`RemoveDebugPage(PageID)`**
- Удаляет страницу из системы
- Автоматически корректирует CurrentPageIndex
- **`SetPageVisibility(PageID, IsVisible)`**
- Управляет видимостью страницы без удаления
#### Навигация
- **`ToggleDebugHUD()`**
- Переключает видимость всего debug интерфейса
- **`NextPage()` / `PreviousPage()`**
- Навигация между видимыми страницами с циклическим переходом
- **`ToggleVisualDebug()`**
- Включение/выключение визуальной отладки (debug draw)
#### Система
- **`InitializeDebugHUD(ToastComponent, InputDeviceComponent)`**
- Инициализация системы с опциональными компонентами
- Создание виджета и подготовка к регистрации страниц
- **`UpdateHUD(CurrentTime)`**
- Основной цикл обновления UI
- Расчет FPS и обновление отображения
- Вызывается из Tick главного персонажа
**Ключевые приватные функции:**
#### Утилиты поиска
- **`FindPageIndex(PageID)`** - Поиск индекса страницы по ID
- **`GetVisiblePages()`** - Получение только видимых страниц
- **`GetCurrentPage()`** - Получение активной страницы
#### Валидация
- **`IsCurrentPageValid(visiblePagesCount)`** - Проверка валидности индекса
- **`IsTimeToUpdate(timeSinceLastUpdate, updateInterval)`** - Проверка времени обновления
- **`IsAtFirstPage()`** - Проверка, является ли текущая страница первой
#### Производительность
- **`ShouldUpdateFPS(currentTime)`** - Проверка необходимости пересчета FPS
- **`UpdateFPSCounter(currentTime)`** - Расчет FPS на основе кадров
#### Виджет управление
- **`GetControlHints()`** - Получение подсказок управления по типу устройства
- **`UpdateWidgetDisplay()`** - Обновление содержимого виджета
- **`GetNavigationText()`** - Генерация текста навигации
- **`CreateDebugWidget()`** - Создание экземпляра виджета
- **`UpdateWidgetVisibility()`** - Обновление видимости виджета
- **`ShouldShowDebugHUD()`** - Проверка условий отображения HUD
### WBP_DebugHUD (UI Widget)
**Ответственности:**
- Отображение debug информации в структурированном виде
- Управление тремя текстовыми секциями: заголовок, контент, навигация
- Автоматическое обновление при изменении данных
**Ключевые функции:**
- `SetHeaderText()` - Установка заголовка текущей страницы
- `SetContentText()` - Обновление основного контента страницы
- `SetNavigationText()` - Отображение информации о навигации и FPS
### S_DebugPage (Data Structure)
**Поля:**
```typescript
{
PageID: string; // Уникальный идентификатор страницы
Title: Text; // Заголовок для отображения
Content: Text; // Текущее содержимое страницы
RefreshRate: Float; // Частота обновления (Hz)
IsVisible: boolean; // Флаг видимости
LastUpdateTime: Float; // Время последнего обновления
}
```
## Workflow использования
### Регистрация debug страницы в компоненте
```typescript
// Movement/Components/AC_Movement.ts
export class AC_Movement extends ActorComponent {
private DebugHUDRef: AC_DebugHUD | null = null;
public BeginPlay(): void {
super.BeginPlay();
// Получаем ссылку на DebugHUD
this.DebugHUDRef = this.GetOwner().FindComponentByClass(AC_DebugHUD);
if (SystemLibrary.IsValid(this.DebugHUDRef)) {
// Регистрируем страницы движения
this.DebugHUDRef.AddDebugPage(
'MovementBasics', // Уникальный ID
'Movement Info', // Заголовок
30, // 30 Hz
true // Видимая
);
this.DebugHUDRef.AddDebugPage(
'MovementPhysics',
'Physics Details',
60 // 60 Hz для высокочастотных данных
);
}
}
public TickComponent(DeltaTime: Float): void {
super.TickComponent(DeltaTime);
// Обновляем свою логику
this.UpdateMovement(DeltaTime);
// Обновляем debug страницы
this.UpdateDebugPages();
}
private UpdateDebugPages(): void {
if (!SystemLibrary.IsValid(this.DebugHUDRef)) return;
const currentTime = this.GetWorld().GetTimeSeconds();
// Проверяем нужно ли обновлять страницу (учитывает RefreshRate)
if (this.DebugHUDRef.ShouldUpdatePage('MovementBasics', currentTime)) {
const content = [
`Speed: ${this.Speed.toFixed(2)} cm/s`,
`Acceleration: ${this.Acceleration.toFixed(2)} cm/s²`,
`Is Grounded: ${this.IsGrounded ? 'Yes' : 'No'}`
].join('\n');
this.DebugHUDRef.UpdatePageContent('MovementBasics', content);
}
if (this.DebugHUDRef.ShouldUpdatePage('MovementPhysics', currentTime)) {
const content = [
`Velocity: ${this.GetVelocity().Size().toFixed(2)} cm/s`,
`Mass: ${this.GetMass().toFixed(2)} kg`,
`Friction: ${this.GetFriction().toFixed(3)}`
].join('\n');
this.DebugHUDRef.UpdatePageContent('MovementPhysics', content);
}
}
}
```
### Инициализация в главном персонаже
```typescript
// Characters/BP_MainCharacter.ts
export class BP_MainCharacter extends Character {
public DebugHUDComponent: AC_DebugHUD;
public BeginPlay(): void {
super.BeginPlay();
// Инициализация DebugHUD (должна быть ПЕРВОЙ)
this.DebugHUDComponent.InitializeDebugHUD(
this.ToastSystemComponent,
this.InputDeviceComponent
);
// После этого все компоненты могут регистрировать свои страницы
}
public Tick(DeltaTime: Float): void {
super.Tick(DeltaTime);
const currentTime = this.GetGameTimeSinceCreation();
// Обновляем только UI, не контент страниц
this.DebugHUDComponent.UpdateHUD(currentTime);
}
}
```
### Динамическое управление страницами
```typescript
// Добавление страницы в runtime
public EnableAdvancedDebug(): void {
if (SystemLibrary.IsValid(this.DebugHUDRef)) {
this.DebugHUDRef.AddDebugPage(
'AdvancedMetrics',
'Advanced Metrics',
120 // Очень высокая частота
);
}
}
// Удаление страницы
public DisableAdvancedDebug(): void {
if (SystemLibrary.IsValid(this.DebugHUDRef)) {
this.DebugHUDRef.RemoveDebugPage('AdvancedMetrics');
}
}
// Скрытие/показ страницы без удаления
public TogglePhysicsDebug(): void {
if (SystemLibrary.IsValid(this.DebugHUDRef)) {
this.showPhysics = !this.showPhysics;
this.DebugHUDRef.SetPageVisibility('MovementPhysics', this.showPhysics);
}
}
```
## Преимущества нового подхода
### ✅ Децентрализация
- Каждый компонент управляет своими debug страницами
- Нет необходимости модифицировать централизованные DataTable или enum'ы
- Компонент владеет логикой генерации своего debug контента
### ✅ Гибкость
- Страницы добавляются/удаляются динамически в runtime
- Легко менять выводимую информацию прямо в компоненте
- Условная регистрация страниц (например, только в Debug билдах)
### ✅ Простота использования
```typescript
// Всего 3 шага:
// 1. Регистрация в BeginPlay
this.DebugHUD.AddDebugPage('MyPage', 'My Title', 30);
// 2. Проверка в Tick
if (this.DebugHUD.ShouldUpdatePage('MyPage', currentTime)) {
// 3. Обновление контента
this.DebugHUD.UpdatePageContent('MyPage', this.GetDebugText());
}
```
### ✅ Индивидуальный контроль производительности
- Каждая страница имеет свой RefreshRate
- Критичные данные: 60-120 Hz
- Обычные данные: 30 Hz
- Статичные данные: 15 Hz или меньше
### ✅ Blueprint-совместимость
- Все параметры - простые типы (string, Text, number, boolean)
- Нет callback'ов или сложных структур данных
- Можно использовать как из C++/TypeScript, так и из Blueprint
## Performance considerations
### Оптимизации
- **Smart update timing:** `ShouldUpdatePage()` автоматически контролирует частоту
- **Single widget update:** UpdateHUD обновляет только текущую видимую страницу
- **Lazy evaluation:** Контент генерируется только когда страница видима и нужно обновление
- **FPS calculation:** Раз в секунду, не влияет на gameplay
### Benchmarks
- **AddDebugPage:** <0.1ms (простое добавление в массив)
- **UpdatePageContent:** <0.05ms (обновление одного элемента массива)
- **ShouldUpdatePage:** <0.05ms (простая проверка времени)
- **UpdateHUD (widget refresh):** <0.2ms (обновление UI элементов)
- **Memory per page:** ~200 bytes (структура + strings)
### Best Practices для производительности
```typescript
// ✅ Хорошо - контролируемое обновление
if (this.DebugHUD.ShouldUpdatePage('MyPage', currentTime)) {
this.DebugHUD.UpdatePageContent('MyPage', this.BuildContent());
}
// ✅ Хорошо - разная частота для разных данных
this.DebugHUD.AddDebugPage('CriticalData', 'Critical', 60); // Частое
this.DebugHUD.AddDebugPage('GeneralInfo', 'General', 30); // Обычное
this.DebugHUD.AddDebugPage('StaticData', 'Static', 5); // Редкое
// ❌ Плохо - обновление без проверки частоты
this.DebugHUD.UpdatePageContent('MyPage', content); // Каждый кадр!
// ❌ Плохо - слишком высокая частота для некритичных данных
this.DebugHUD.AddDebugPage('SlowData', 'Slow', 120); // Избыточно
```
## Система тестирования
### FT_DebugSystem (Basic Functionality)
**Покрывает:**
- Успешность инициализации системы (`IsInitialized = true`)
- Валидность компонента DebugHUD после инициализации
- Корректность создания виджета
- Базовую функциональность регистрации страниц
**Тестовый сценарий:**
```typescript
1. Инициализация DebugHUD
2. Проверка IsInitialized == true
3. Проверка валидности компонента через SystemLibrary.IsValid()
```
### FT_DebugNavigation (Navigation System)
**Покрывает:**
- Корректность индексации при навигации
- Валидность CurrentPageIndex после NextPage/PreviousPage
- Циклическое поведение при достижении границ
- Устойчивость к многократным переходам
**Тестовый сценарий:**
```typescript
1. Инициализация с проверкой начального состояния
2. NextPage() → проверка индекса в пределах [0, VisiblePages.length)
3. PreviousPage() → проверка индекса в пределах [0, VisiblePages.length)
4. Множественные переходы → индекс всегда валидный
```
**Валидация состояния:**
```typescript
private IsStateValid(): boolean {
const { VisiblePagesLength, CurrentPageIndex } = this.DebugHUD.GetTestData();
return (
VisiblePagesLength > 0 &&
CurrentPageIndex >= 0 &&
CurrentPageIndex < VisiblePagesLength
);
}
```
### FT_DebugPageManagement (NEW - Page Operations)
**Покрывает:**
- Динамическое добавление страниц через AddDebugPage
- Обновление контента через UpdatePageContent
- Проверку частоты обновления через ShouldUpdatePage
- Удаление страниц через RemoveDebugPage
- Управление видимостью через SetPageVisibility
**Тестовый сценарий:**
```typescript
1. AddDebugPage('TestPage1', 'Test', 30)
→ Проверка что страница добавлена (DebugPages.length == 1)
2. UpdatePageContent('TestPage1', 'New Content')
→ Проверка что контент обновился
3. ShouldUpdatePage('TestPage1', time)
→ Проверка что возвращает true при первом вызове
→ Проверка что возвращает false сразу после
4. AddDebugPage('TestPage2', 'Test2', 60)
→ Проверка что страница добавлена (DebugPages.length == 2)
5. SetPageVisibility('TestPage2', false)
→ Проверка что VisiblePages.length == 1
6. RemoveDebugPage('TestPage1')
→ Проверка что страница удалена (DebugPages.length == 1)
→ Проверка что CurrentPageIndex корректно обновился
```
## Структура файлов
```
Content/
├── Debug/
│ ├── Components/
│ │ └── AC_DebugHUD.ts # Main debug system component
│ ├── Structs/
│ │ └── S_DebugPage.ts # Page data structure
│ ├── UI/
│ │ └── WBP_DebugHUD.ts # Debug HUD widget
│ └── Tests/
│ ├── FT_DebugSystem.ts # Basic functionality tests
│ ├── FT_DebugNavigation.ts # Navigation system tests
│ └── FT_DebugPageManagement.ts # Page operations tests (NEW)
├── Input/
│ └── IMC_Default.ts # Input mapping integration
└── Characters/
└── BP_MainCharacter.ts # Main integration point
```
## Примеры использования из разных компонентов
### Camera Component Debug
```typescript
export class AC_Camera extends ActorComponent {
private DebugHUDRef: AC_DebugHUD | null = null;
public BeginPlay(): void {
super.BeginPlay();
this.DebugHUDRef = this.GetOwner().FindComponentByClass(AC_DebugHUD);
if (SystemLibrary.IsValid(this.DebugHUDRef)) {
this.DebugHUDRef.AddDebugPage(
'CameraInfo',
'Camera State',
30
);
}
}
public TickComponent(DeltaTime: Float): void {
super.TickComponent(DeltaTime);
if (!SystemLibrary.IsValid(this.DebugHUDRef)) return;
const currentTime = this.GetWorld().GetTimeSeconds();
if (this.DebugHUDRef.ShouldUpdatePage('CameraInfo', currentTime)) {
const content = [
`FOV: ${this.GetFOV().toFixed(1)}°`,
`Distance: ${this.GetCameraDistance().toFixed(2)} cm`,
`Pitch: ${this.GetPitch().toFixed(1)}°`,
`Yaw: ${this.GetYaw().toFixed(1)}°`,
`Target: ${this.GetTargetLocation().ToString()}`
].join('\n');
this.DebugHUDRef.UpdatePageContent('CameraInfo', content);
}
}
}
```
### Network Component Debug
```typescript
export class AC_NetworkReplication extends ActorComponent {
private DebugHUDRef: AC_DebugHUD | null = null;
public BeginPlay(): void {
super.BeginPlay();
this.DebugHUDRef = this.GetOwner().FindComponentByClass(AC_DebugHUD);
if (SystemLibrary.IsValid(this.DebugHUDRef)) {
// Регистрируем только в сетевой игре
if (this.GetWorld().IsNetMode()) {
this.DebugHUDRef.AddDebugPage(
'NetworkStats',
'Network Statistics',
15 // Обновляем реже для сетевых данных
);
}
}
}
public TickComponent(DeltaTime: Float): void {
super.TickComponent(DeltaTime);
if (!SystemLibrary.IsValid(this.DebugHUDRef)) return;
const currentTime = this.GetWorld().GetTimeSeconds();
if (this.DebugHUDRef.ShouldUpdatePage('NetworkStats', currentTime)) {
const content = [
`Ping: ${this.GetPing()}ms`,
`Packet Loss: ${this.GetPacketLoss().toFixed(2)}%`,
`Bandwidth: ${this.GetBandwidth().toFixed(1)} KB/s`,
`Connected: ${this.IsConnected() ? 'Yes' : 'No'}`,
`Players: ${this.GetPlayerCount()}`
].join('\n');
this.DebugHUDRef.UpdatePageContent('NetworkStats', content);
}
}
}
```
### Conditional Debug Pages
```typescript
export class AC_AdvancedSystem extends ActorComponent {
private DebugHUDRef: AC_DebugHUD | null = null;
private showDetailedDebug: boolean = false;
public BeginPlay(): void {
super.BeginPlay();
this.DebugHUDRef = this.GetOwner().FindComponentByClass(AC_DebugHUD);
if (SystemLibrary.IsValid(this.DebugHUDRef)) {
// Базовая страница всегда
this.DebugHUDRef.AddDebugPage('BasicInfo', 'Basic Info', 30);
// Детальная только в Debug билде
if (BUILD_DEBUG) {
this.DebugHUDRef.AddDebugPage(
'DetailedInfo',
'Detailed Debug',
120,
false // Скрыта по умолчанию
);
}
}
}
public ToggleDetailedDebug(): void {
this.showDetailedDebug = !this.showDetailedDebug;
if (SystemLibrary.IsValid(this.DebugHUDRef)) {
this.DebugHUDRef.SetPageVisibility('DetailedInfo', this.showDetailedDebug);
}
}
}
```
## Известные ограничения
### Текущие ограничения
1. **Текстовый контент только** - Нет поддержки графиков, диаграмм, интерактивных элементов
2. **Фиксированный layout** - Трехсекционный layout (header, content, navigation) не настраивается
3. **Линейная навигация** - Только последовательный переход между страницами
4. **Глобальный FPS** - Один FPS counter для всей системы
### Архитектурные решения
1. **Компоненты управляют своими страницами** - Каждый компонент отвечает за регистрацию и обновление
2. **String-based PageID** - Простота использования в ущерб типобезопасности
3. **Tick-based updates** - Компоненты обновляют страницы в своем Tick
4. **No data caching** - Контент генерируется при каждом обновлении
## Миграция со старого подхода
### Было (DT_DebugPages + E_DebugUpdateFunction)

View File

@ -1,107 +0,0 @@
// Debug/Tests/FT_DebugNavigation.ts
import { AC_DebugHUD } from '#root/Debug/Components/AC_DebugHUD.ts';
import { AC_InputDevice } from '#root/Input/Components/AC_InputDevice.ts';
import { AC_ToastSystem } from '#root/Toasts/Components/AC_ToastSystem.ts';
import { EFunctionalTestResult } from '#root/UE/EFunctionalTestResult.ts';
import { FunctionalTest } from '#root/UE/FunctionalTest.ts';
import type { Integer } from '#root/UE/Integer.ts';
/**
* Functional Test: Debug HUD Navigation System
* Tests page navigation state management during NextPage/PreviousPage operations
*/
export class FT_DebugNavigation extends FunctionalTest {
// ════════════════════════════════════════════════════════════════════════════════════════
// GRAPHS
// ════════════════════════════════════════════════════════════════════════════════════════
// ────────────────────────────────────────────────────────────────────────────────────────
// EventGraph
// ────────────────────────────────────────────────────────────────────────────────────────
/**
* Test entry point - validates navigation state during page operations
* Uses nested validation to ensure CurrentPageIndex stays valid
*/
EventStartTest(): void {
this.DebugHUDComponent.InitializeDebugHUD(
this.ToastSystemComponent,
this.InputDeviceComponent
);
this.IfValid('Debug HUD: Navigation invalid initial state', () => {
this.IfValid(
'Debug HUD: NextPage failed — Invalid state before NextPage',
() => {
this.DebugHUDComponent.NextPage();
this.IfValid(
'Debug HUD: NextPage failed — State became invalid after NextPage',
() => {
this.DebugHUDComponent.PreviousPage();
this.IfValid(
'Debug HUD: PrevPage failed — State became invalid after PreviousPage',
() => {
this.FinishTest(EFunctionalTestResult.Succeeded);
}
);
}
);
}
);
});
}
// ════════════════════════════════════════════════════════════════════════════════════════
// MACROS
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Validates current page index and executes callback if state is valid
* @param Message - Error message if validation fails
* @param Out - Callback to execute if state is valid
*/
private IfValid(Message: string, Out: () => void): void {
const IsPageIndexOutOfBounds = (
visiblePagesLength: Integer,
currentPage: Integer
): boolean => visiblePagesLength > 0 && currentPage >= visiblePagesLength;
if (
!IsPageIndexOutOfBounds(
this.DebugHUDComponent.GetTestData().VisiblePagesLength,
this.DebugHUDComponent.GetTestData().CurrentPageIndex
) &&
this.DebugHUDComponent.GetTestData().CurrentPageIndex >= 0
) {
Out();
} else {
this.FinishTest(EFunctionalTestResult.Failed, Message);
}
}
// ════════════════════════════════════════════════════════════════════════════════════════
// VARIABLES
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Debug HUD system - primary component under test
* Tests page navigation state management
* @category Components
*/
DebugHUDComponent = new AC_DebugHUD();
/**
* Toast notification system - required for debug HUD initialization
* @category Components
*/
ToastSystemComponent = new AC_ToastSystem();
/**
* Input device detection system - used for input device debug page testing
* @category Components
*/
InputDeviceComponent = new AC_InputDevice();
}

Binary file not shown.

View File

@ -1,261 +0,0 @@
// Debug/Tests/FT_DebugPageManagement.ts
import { AC_DebugHUD } from '#root/Debug/Components/AC_DebugHUD.ts';
import { AC_InputDevice } from '#root/Input/Components/AC_InputDevice.ts';
import { AC_ToastSystem } from '#root/Toasts/Components/AC_ToastSystem.ts';
import { EFunctionalTestResult } from '#root/UE/EFunctionalTestResult.ts';
import { FunctionalTest } from '#root/UE/FunctionalTest.ts';
import { SystemLibrary } from '#root/UE/SystemLibrary.ts';
/**
* Functional Test: Debug Page Management
* Tests dynamic page registration, content updates, and lifecycle operations
*/
export class FT_DebugPageManagement extends FunctionalTest {
// ════════════════════════════════════════════════════════════════════════════════════════
// GRAPHS
// ════════════════════════════════════════════════════════════════════════════════════════
// ────────────────────────────────────────────────────────────────────────────────────────
// EventGraph
// ────────────────────────────────────────────────────────────────────────────────────────
/**
* Test entry point - validates all page management operations
* Tests: Add, Update, ShouldUpdate, Visibility, Remove
*/
EventStartTest(): void {
this.DebugHUDComponent.InitializeDebugHUD(
this.ToastSystemComponent,
this.InputDeviceComponent
);
this.DebugHUDComponent.AddDebugPage('TestPage1', 'Test Page 1', 30, true);
const pageCount = this.DebugHUDComponent.GetTestData().DebugPages.length;
if (pageCount === 1) {
const testContent = 'Test Content 123';
this.DebugHUDComponent.UpdatePageContent('TestPage1', testContent);
const page = this.DebugHUDComponent.GetTestData().DebugPages.Get(0);
const contentMatches = page.Content === testContent;
if (contentMatches) {
const currentTime = SystemLibrary.GetGameTimeInSeconds();
// First call should return true (no previous update)
const firstCall = this.DebugHUDComponent.ShouldUpdatePage(
'TestPage1',
currentTime
);
// Immediate second call should return false (just updated)
const secondCall = this.DebugHUDComponent.ShouldUpdatePage(
'TestPage1',
currentTime
);
if (firstCall && !secondCall) {
this.DebugHUDComponent.AddDebugPage(
'TestPage2',
'Test Page 2',
60,
true
);
const pageCount =
this.DebugHUDComponent.GetTestData().DebugPages.length;
const visibleCount =
this.DebugHUDComponent.GetTestData().VisiblePagesLength;
if (pageCount === 2 && visibleCount === 2) {
// Hide second page
this.DebugHUDComponent.SetPageVisibility('TestPage2', false);
const totalCount =
this.DebugHUDComponent.GetTestData().DebugPages.length;
const visibleCount =
this.DebugHUDComponent.GetTestData().VisiblePagesLength;
if (totalCount === 2 && visibleCount === 1) {
// Remove first page
this.DebugHUDComponent.RemoveDebugPage('TestPage1');
const totalCount =
this.DebugHUDComponent.GetTestData().DebugPages.length;
const currentIndex =
this.DebugHUDComponent.GetTestData().CurrentPageIndex;
if (totalCount === 1 && currentIndex === 0) {
// Re-add page with same ID but different settings
this.DebugHUDComponent.AddDebugPage(
'TestPage2',
'Updated Title',
120,
true
);
const totalCount =
this.DebugHUDComponent.GetTestData().DebugPages.length;
const visibleCount =
this.DebugHUDComponent.GetTestData().VisiblePagesLength;
const page =
this.DebugHUDComponent.GetTestData().DebugPages.Get(0);
const titleMatches = page.Title === 'Updated Title';
const refreshRateMatches = page.RefreshRate === 120;
if (
totalCount === 1 &&
visibleCount === 1 &&
titleMatches &&
refreshRateMatches
) {
// Add pages with different refresh rates
this.DebugHUDComponent.AddDebugPage(
'FastPage',
'Fast Page',
120,
true
);
this.DebugHUDComponent.AddDebugPage(
'SlowPage',
'Slow Page',
10,
true
);
const currentTime = SystemLibrary.GetGameTimeInSeconds();
// Both should update on first call
const fastShouldUpdate =
this.DebugHUDComponent.ShouldUpdatePage(
'FastPage',
currentTime
);
const slowShouldUpdate =
this.DebugHUDComponent.ShouldUpdatePage(
'SlowPage',
currentTime
);
// Wait for fast page interval (1/120 = 0.0083s) but not slow (1/10 = 0.1s)
const fastUpdateTime = currentTime + 0.01;
const fastShouldUpdateAgain =
this.DebugHUDComponent.ShouldUpdatePage(
'FastPage',
fastUpdateTime
);
const slowShouldNotUpdate =
this.DebugHUDComponent.ShouldUpdatePage(
'SlowPage',
fastUpdateTime
);
if (
fastShouldUpdate &&
slowShouldUpdate &&
fastShouldUpdateAgain &&
!slowShouldNotUpdate
) {
// Try to update non-existent page (should not crash)
this.DebugHUDComponent.UpdatePageContent(
'NonExistentPage',
'Test'
);
// Try to remove non-existent page (should not crash)
this.DebugHUDComponent.RemoveDebugPage('NonExistentPage');
// Try to check non-existent page (should return false)
const currentTime = SystemLibrary.GetGameTimeInSeconds();
const shouldUpdate =
this.DebugHUDComponent.ShouldUpdatePage(
'NonExistentPage',
currentTime
);
if (!shouldUpdate) {
this.FinishTest(EFunctionalTestResult.Succeeded);
} else {
this.FinishTest(
EFunctionalTestResult.Failed,
'Test 9 Failed: ShouldUpdatePage returned true for non-existent page'
);
}
} else {
this.FinishTest(
EFunctionalTestResult.Failed,
`Test 8 Failed: Refresh rates incorrect (fast1: ${fastShouldUpdate}, slow1: ${slowShouldUpdate}, fast2: ${fastShouldUpdateAgain}, slow2: ${slowShouldNotUpdate})`
);
}
} else {
this.FinishTest(
EFunctionalTestResult.Failed,
`Test 7 Failed: Update registration incorrect (count: ${totalCount}, title: ${titleMatches}, rate: ${refreshRateMatches})`
);
}
} else {
this.FinishTest(
EFunctionalTestResult.Failed,
`Test 6 Failed: Remove incorrect (count: ${totalCount}, index: ${currentIndex})`
);
}
} else {
this.FinishTest(
EFunctionalTestResult.Failed,
`Test 5 Failed: Visibility incorrect (total: ${totalCount}, visible: ${visibleCount})`
);
}
} else {
this.FinishTest(
EFunctionalTestResult.Failed,
`Test 4 Failed: Expected 2 pages (total: ${pageCount}, visible: ${visibleCount})`
);
}
} else {
this.FinishTest(
EFunctionalTestResult.Failed,
`Test 3 Failed: ShouldUpdatePage timing incorrect (first: ${firstCall}, second: ${secondCall})`
);
}
} else {
this.FinishTest(
EFunctionalTestResult.Failed,
'Test 2 Failed: Content did not update correctly'
);
}
} else {
this.FinishTest(
EFunctionalTestResult.Failed,
`Test 1 Failed: Expected 1 page, got ${pageCount}`
);
}
}
// ════════════════════════════════════════════════════════════════════════════════════════
// VARIABLES
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Debug HUD system - primary component under test
* Tests all page management operations
* @category Components
*/
DebugHUDComponent = new AC_DebugHUD();
/**
* Toast notification system - required for debug HUD initialization
* @category Components
*/
ToastSystemComponent = new AC_ToastSystem();
/**
* Input device detection system - required for debug HUD initialization
* @category Components
*/
InputDeviceComponent = new AC_InputDevice();
}

Binary file not shown.

View File

@ -1,72 +0,0 @@
// Debug/Tests/FT_DebugSystem.ts
import { AC_DebugHUD } from '#root/Debug/Components/AC_DebugHUD.ts';
import { AC_InputDevice } from '#root/Input/Components/AC_InputDevice.ts';
import { AC_ToastSystem } from '#root/Toasts/Components/AC_ToastSystem.ts';
import { EFunctionalTestResult } from '#root/UE/EFunctionalTestResult.ts';
import { FunctionalTest } from '#root/UE/FunctionalTest.ts';
import { SystemLibrary } from '#root/UE/SystemLibrary.ts';
/**
* Functional Test: Debug System Basic Functionality
* Validates initialization, component validity, and data table consistency
*/
export class FT_DebugSystem extends FunctionalTest {
// ════════════════════════════════════════════════════════════════════════════════════════
// GRAPHS
// ════════════════════════════════════════════════════════════════════════════════════════
// ────────────────────────────────────────────────────────────────────────────────────────
// EventGraph
// ────────────────────────────────────────────────────────────────────────────────────────
/**
* Test entry point - validates basic debug system functionality
* Uses nested validation to check initialization, page count, and component validity
*/
EventStartTest(): void {
this.DebugHUDComponent.InitializeDebugHUD(
this.ToastSystemComponent,
this.InputDeviceComponent
);
if (this.DebugHUDComponent.GetTestData().IsInitialized) {
if (SystemLibrary.IsValid(this.DebugHUDComponent)) {
this.FinishTest(EFunctionalTestResult.Succeeded);
} else {
this.FinishTest(
EFunctionalTestResult.Failed,
'DebugHUD component not valid'
);
}
} else {
this.FinishTest(
EFunctionalTestResult.Failed,
'Debug HUD failed to initialize'
);
}
}
// ════════════════════════════════════════════════════════════════════════════════════════
// VARIABLES
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Debug HUD system - primary component under test
* Tests basic system initialization and component validity
* @category Components
*/
DebugHUDComponent = new AC_DebugHUD();
/**
* Toast notification system - required for debug HUD initialization
* @category Components
*/
ToastSystemComponent = new AC_ToastSystem();
/**
* Input device detection system - used for input device debug page testing
* @category Components
*/
InputDeviceComponent = new AC_InputDevice();
}

BIN
Content/Debug/Tests/FT_DebugSystem.uasset (Stored with Git LFS)

Binary file not shown.

View File

@ -1,10 +1,9 @@
// Debug/UI/WBP_DebugHUD.ts
// Content/Debug/UI/WBP_DebugHUD.ts
import type { AC_Movement } from '#root/Movement/Components/AC_Movement.js';
import { SystemLibrary } from '#root/UE/SystemLibrary.ts';
import type { Text } from '#root/UE/Text.ts';
import { TextBlock } from '#root/UE/TextBlock.ts';
import { UserWidget } from '#root/UE/UserWidget.ts';
import { SystemLibrary } from '/Content/UE/SystemLibrary.ts';
import type { Text } from '/Content/UE/Text.ts';
import { TextBlock } from '/Content/UE/TextBlock.ts';
import { UserWidget } from '/Content/UE/UserWidget.ts';
/**
* Debug HUD Widget for displaying system information
@ -107,13 +106,6 @@ export class WBP_DebugHUD extends UserWidget {
// VARIABLES
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Reference to movement component for accessing debug data
* Set by AC_DebugHUD during initialization
* @category Components
*/
public MovementComponent: AC_Movement | null = null;
/**
* Current page title text
* Updated by AC_DebugHUD when switching pages

BIN
Content/Debug/UI/WBP_DebugHUD.uasset (Stored with Git LFS)

Binary file not shown.

View File

@ -1,6 +1,6 @@
// Input/Actions/IA_LeftTrigger.ts
// Content/Input/Actions/IA_LeftTrigger.ts
import { InputAction } from '#root/UE/InputAction.ts';
import { Name } from '#root/UE/Name.ts';
import { InputAction } from '/Content/UE/InputAction.ts';
import { Name } from '/Content/UE/Name.ts';
export const IA_LeftTrigger = new InputAction(null, new Name('IA_LeftTrigger'));

View File

@ -1,6 +1,6 @@
// Input/Actions/IA_Look.ts
// Content/Input/Actions/IA_Look.ts
import { InputAction } from '#root/UE/InputAction.ts';
import { Name } from '#root/UE/Name.ts';
import { InputAction } from '/Content/UE/InputAction.ts';
import { Name } from '/Content/UE/Name.ts';
export const IA_Look = new InputAction(null, new Name('IA_Look'));

View File

@ -1,6 +1,6 @@
// Input/Actions/IA_Move.ts
// Content/Input/Actions/IA_Move.ts
import { InputAction } from '#root/UE/InputAction.ts';
import { Name } from '#root/UE/Name.ts';
import { InputAction } from '/Content/UE/InputAction.ts';
import { Name } from '/Content/UE/Name.ts';
export const IA_Move = new InputAction(null, new Name('IA_Move'));

View File

@ -1,5 +1,5 @@
// Input/Actions/IA_NextDebugMode.ts
// Content/Input/Actions/IA_NextDebugMode.ts
import { InputAction } from '#root/UE/InputAction.ts';
import { InputAction } from '/Content/UE/InputAction.ts';
export const IA_NextDebugMode = new InputAction(null, 'IA_NextDebugMode');

View File

@ -1,5 +1,5 @@
// Input/Actions/IA_PrevDebugMode.ts
// Content/Input/Actions/IA_PrevDebugMode.ts
import { InputAction } from '#root/UE/InputAction.ts';
import { InputAction } from '/Content/UE/InputAction.ts';
export const IA_PrevDebugMode = new InputAction(null, 'IA_PrevDebugMode');

View File

@ -1,5 +1,5 @@
// Input/Actions/IA_RightTrigger.ts
// Content/Input/Actions/IA_RightTrigger.ts
import { InputAction } from '#root/UE/InputAction.ts';
import { InputAction } from '/Content/UE/InputAction.ts';
export const IA_RightTrigger = new InputAction(null, 'IA_RightTrigger');

View File

@ -1,5 +1,5 @@
// Input/Actions/IA_ToggleHUD.ts
// Content/Input/Actions/IA_ToggleHUD.ts
import { InputAction } from '#root/UE/InputAction.ts';
import { InputAction } from '/Content/UE/InputAction.ts';
export const IA_ToggleHUD = new InputAction(null, 'IA_ToggleHUD');

View File

@ -1,6 +1,6 @@
// Input/Actions/IA_ToggleVisualDebug.ts
// Content/Input/Actions/IA_ToggleVisualDebug.ts
import { InputAction } from '#root/UE/InputAction.ts';
import { InputAction } from '/Content/UE/InputAction.ts';
export const IA_ToggleVisualDebug = new InputAction(
null,

View File

@ -1,14 +1,14 @@
// Input/Components/AC_InputDevice.ts
// Content/Input/Components/AC_InputDevice.ts
import type { AC_DebugHUD } from '#root/Debug/Components/AC_DebugHUD.ts';
import type { AC_ToastSystem } from '#root/Toasts/Components/AC_ToastSystem.ts';
import { ActorComponent } from '#root/UE/ActorComponent.ts';
import { EHardwareDevicePrimaryType } from '#root/UE/EHardwareDevicePrimaryType.ts';
import type { Float } from '#root/UE/Float.ts';
import { InputDeviceSubsystem } from '#root/UE/InputDeviceSubsystem.ts';
import type { Integer } from '#root/UE/Integer.ts';
import { SystemLibrary } from '#root/UE/SystemLibrary.ts';
import { E_MessageType } from '#root/UI/Enums/E_MessageType.ts';
import type { AC_DebugHUD } from '/Content/Debug/Components/AC_DebugHUD.ts';
import type { AC_ToastSystem } from '/Content/Toasts/Components/AC_ToastSystem.ts';
import { ActorComponent } from '/Content/UE/ActorComponent.ts';
import { EHardwareDevicePrimaryType } from '/Content/UE/EHardwareDevicePrimaryType.ts';
import type { Float } from '/Content/UE/Float.ts';
import { InputDeviceSubsystem } from '/Content/UE/InputDeviceSubsystem.ts';
import type { Integer } from '/Content/UE/Integer.ts';
import { SystemLibrary } from '/Content/UE/SystemLibrary.ts';
import { E_MessageType } from '/Content/UI/Enums/E_MessageType.ts';
/**
* Input Device Detection Component

View File

@ -1,15 +1,15 @@
// Input/IMC_Default.ts
// Content/Input/IMC_Default.ts
import { IA_LeftTrigger } from '#root/Input/Actions/IA_LeftTrigger.ts';
import { IA_Look } from '#root/Input/Actions/IA_Look.ts';
import { IA_Move } from '#root/Input/Actions/IA_Move.ts';
import { IA_NextDebugMode } from '#root/Input/Actions/IA_NextDebugMode.ts';
import { IA_PrevDebugMode } from '#root/Input/Actions/IA_PrevDebugMode.ts';
import { IA_RightTrigger } from '#root/Input/Actions/IA_RightTrigger.ts';
import { IA_ToggleHUD } from '#root/Input/Actions/IA_ToggleHUD.ts';
import { IA_ToggleVisualDebug } from '#root/Input/Actions/IA_ToggleVisualDebug.ts';
import { InputMappingContext } from '#root/UE/InputMappingContext.ts';
import { Key } from '#root/UE/Key.ts';
import { IA_LeftTrigger } from '/Content/Input/Actions/IA_LeftTrigger.ts';
import { IA_Look } from '/Content/Input/Actions/IA_Look.ts';
import { IA_Move } from '/Content/Input/Actions/IA_Move.ts';
import { IA_NextDebugMode } from '/Content/Input/Actions/IA_NextDebugMode.ts';
import { IA_PrevDebugMode } from '/Content/Input/Actions/IA_PrevDebugMode.ts';
import { IA_RightTrigger } from '/Content/Input/Actions/IA_RightTrigger.ts';
import { IA_ToggleHUD } from '/Content/Input/Actions/IA_ToggleHUD.ts';
import { IA_ToggleVisualDebug } from '/Content/Input/Actions/IA_ToggleVisualDebug.ts';
import { InputMappingContext } from '/Content/UE/InputMappingContext.ts';
import { Key } from '/Content/UE/Key.ts';
export const IMC_Default = new InputMappingContext();

View File

@ -1,75 +0,0 @@
[//]: # (Input/ManualTestingChecklist.md)
# Input Device System - Manual Testing Checklist
## Тестовая среда
- **Персонаж:** BP_MainCharacter с ShowDebugInfo = true
- **Клавиши:** PageUp/PageDown для навигации в Debug HUD
- **Требования:** InputDeviceComponent инициализирован
---
## 1. Debug HUD Integration
### 1.1 Input Device Info Page
- [ ] **Page 4** отображается как "Input Device Detection"
- [ ] **PageUp/PageDown** позволяет перейти на Input Device page
- [ ] **Содержимое страницы** показывает:
- Primary Type: [тип устройства UE]
- Is Initialized: [true/false]
### 1.2 Real-time Device Detection
- [ ] **При использовании мыши/клавиатуры** Primary Type показывает "Keyboard & Mouse"
- [ ] **При подключении геймпада** Primary Type автоматически меняется на "Gamepad"
---
## 2. Автоматическая детекция устройств
### 2.1 Keyboard & Mouse Detection
- [ ] **Движение мыши** автоматически переключает на Keyboard & Mouse
- [ ] **Нажатие клавиш** (WASD, пробел, etc.) переключает на Keyboard & Mouse
- [ ] **Primary Type** показывает "KeyboardAndMouse"
### 2.2 Gamepad Detection
- [ ] **Движение стиков** автоматически переключает на Gamepad
- [ ] **Нажатие кнопок геймпада** переключает на Gamepad
- [ ] **Primary Type** показывает "Gamepad"
---
## 3. API Functions Testing
### 3.1 Device Type Queries (Binary)
- [ ] **IsKeyboard()** возвращает true для всех устройств кроме Gamepad
- [ ] **IsGamepad()** возвращает true только для геймпадов
- [ ] **IsKeyboard() и IsGamepad()** никогда не возвращают одинаковые значения
- [ ] **GetCurrentInputDevice()** возвращает корректный EHardwareDevicePrimaryType
---
## 4. Error Handling
### 4.1 Edge Cases
- [ ] **Отключение устройств** обрабатывается корректно
- [ ] **Подключение новых устройств** детектируется автоматически
- [ ] **System console** не содержит ошибок input detection
- [ ] **Performance** остается стабильной при активном использовании
### 4.2 Integration Stability
- [ ] **Debug HUD** стабильно работает с device detection
- [ ] **Частые переключения** устройств не вызывают проблем
- [ ] **AC_InputDevice** корректно инициализируется
- [ ] **IsGamepad/IsKeyboard** всегда возвращают корректные значения
---
## Критерии прохождения
- [ ] All device types correctly detected and displayed
- [ ] Real-time switching works seamlessly through UE subsystem
- [ ] Debug HUD shows complete hardware information
- [ ] No console errors during normal operation
- [ ] API functions return consistent results
- [ ] Native UE InputDeviceSubsystem integration works properly
**Примечание:** Система использует только встроенную InputDeviceSubsystem от Unreal Engine. Никаких симуляций или искусственных переключений.

View File

@ -1,411 +0,0 @@
[//]: # (Input/TDD.md)
# Input Device Detection System - Техническая Документация
## Обзор
Event-driven система определения типа устройства ввода, основанная на делегате OnInputHardwareDeviceChanged от Unreal Engine 5.3+. Предоставляет простую бинарную классификацию устройств с automatic debouncing и минимальным overhead при отсутствии смены устройства.
## Архитектурные принципы
- **Event-Driven Detection:** Использование OnInputHardwareDeviceChanged delegate вместо polling
- **Binary Simplicity:** Только два состояния - геймпад или клавиатура/мышь
- **Automatic Debouncing:** Встроенная защита от rapid device switching
- **Zero Polling Overhead:** Реакция только на реальные события смены устройства
## Единственный компонент
### AC_InputDevice (Event-Driven Wrapper)
**Ответственности:**
- Event-driven обертка над Unreal Engine InputDeviceSubsystem
- Automatic debouncing для предотвращения flickering
- Бинарная классификация: IsGamepad() vs IsKeyboard()
- Интеграция с Toast notification system для debug
**Ключевые функции:**
- `InitializeDeviceDetection()` - регистрация delegate и initial detection
- `IsKeyboard()` / `IsGamepad()` - binary device queries
- `GetCurrentInputDevice()` - доступ к cached device state
- `OnInputHardwareDeviceChanged()` - event handler для device switching
**Event-driven архитектура:**
```typescript
InputDeviceSubsystem.OnInputHardwareDeviceChanged.BindEvent()
→ OnInputHardwareDeviceChanged()
→ ProcessDeviceChange()
→ Update cached state + Toast notification
```
## Система событий и debouncing
### Event Registration
```typescript
// Регистрация event handler при инициализации
InputDeviceSubsystem.OnInputHardwareDeviceChanged.BindEvent(
this.OnInputHardwareDeviceChanged.bind(this)
);
```
### Event Processing Flow
```typescript
OnInputHardwareDeviceChanged(UserId, DeviceId) →
GetInputDeviceHardwareIdentifier(DeviceId) →
ProcessDeviceChange(PrimaryDeviceType) →
CanProcessDeviceChange() (debouncing check) →
Update CurrentDevice + LastChangeTime →
Toast notification
```
### Automatic Debouncing
```typescript
// Защита от rapid switching
private CanProcessDeviceChange(): boolean {
const HasCooldownExpired = (): boolean =>
SystemLibrary.GetGameTimeInSeconds() - this.LastDeviceChangeTime >=
this.DeviceChangeCooldown; // 300ms по умолчанию
return HasCooldownExpired();
}
```
## Классификация устройств
### Binary Device Logic
```typescript
// Вся логика классификации:
IsGamepad() → CurrentDevice === EHardwareDevicePrimaryType.Gamepad
IsKeyboard() → CurrentDevice === EHardwareDevicePrimaryType.KeyboardAndMouse
```
### Device Detection через Hardware Names
```typescript
// Определение типа устройства по событию:
OnInputHardwareDeviceChanged(UserId, DeviceId) →
InputDeviceSubsystem.GetInputDeviceHardwareIdentifier(DeviceId) →
.PrimaryDeviceType →
Update CurrentDevice state
```
### Mapping UE типов
```typescript
// Поддерживаемые устройства:
EHardwareDevicePrimaryType.Gamepad → IsGamepad() = true
EHardwareDevicePrimaryType.KeyboardAndMouse → IsKeyboard() = true
EHardwareDevicePrimaryType.Unspecified → fallback to previous state
```
## Производительность
### Event-Driven преимущества
- **Zero polling overhead:** Обновления только при реальных событиях
- **Instant response:** Мгновенная реакция на device switching
- **Minimal CPU usage:** Нет постоянных проверок в Tick
- **Automatic state management:** UE Engine управляет device state
### Benchmarks
- **Инициализация:** <0.1ms (регистрация delegate + initial detection)
- **Event processing:** <0.05ms на событие (с debouncing)
- **IsKeyboard/IsGamepad:** <0.001ms (cached state access)
- **Memory footprint:** ~50 bytes (cached state + timers)
### Performance considerations
- **Event frequency:** Обычно 0-5 событий в секунду при активном switching
- **Debouncing cost:** Одно сравнение float времени на событие
- **No allocations:** Все операции работают с existing objects
- **Toast overhead:** Optional debug notifications не влияют на core performance
## Debouncing система
### Cooldown механизм
```typescript
private DeviceChangeCooldown: Float = 0.3; // 300ms стандартный интервал
private LastDeviceChangeTime: Float = 0; // Timestamp последней смены
// Проверка при каждом событии:
if (SystemLibrary.GetGameTimeInSeconds() - LastDeviceChangeTime >= DeviceChangeCooldown) {
// Process device change
} else {
// Ignore rapid switching
}
```
### Защита от stick drift
Event-driven подход естественно защищает от большинства stick drift проблем:
- **Hardware events** срабатывают реже чем input polling
- **Debouncing** отфильтровывает rapid oscillation
- **Real device changes** (кнопки, отключение) проходят через систему
## Интеграция с системами
### С Toast System
```typescript
// Debug notifications при смене устройства
if (SystemLibrary.IsValid(this.ToastComponent)) {
this.ToastComponent.ShowToast(
`Input switched to ${NewDevice}`,
E_MessageType.Info
);
}
```
### С Debug HUD System
```typescript
// Новая debug page для input device info:
UpdateInputDevicePage(): string {
const deviceType = this.InputDeviceComponent.IsGamepad() ? 'Gamepad' : 'Keyboard & Mouse';
const isInitialized = this.InputDeviceComponent.IsInitialized ? 'Yes' : 'No';
return `Current Device: ${deviceType}\n` +
`Initialized: ${isInitialized}\n` +
`Last Change: ${this.GetTimeSinceLastChange()}s ago`;
}
```
### С Enhanced Input System (будущая интеграция)
```typescript
// Этап 6+: Input Mapping Context switching
OnDeviceChanged() → Branch: IsGamepad()?
True → Remove IMC_Keyboard + Add IMC_Gamepad
False → Remove IMC_Gamepad + Add IMC_Keyboard
```
## API Reference
### Основные методы
#### InitializeDeviceDetection()
```typescript
InitializeDeviceDetection(ToastComponentRef: AC_ToastSystem): void
```
**Описание:** Инициализация event-driven device detection
**Параметры:** ToastComponentRef для debug notifications
**Когда вызывать:** EventBeginPlay в main character
**Эффекты:** Регистрирует delegate, выполняет initial detection, показывает success toast
#### IsKeyboard()
```typescript
IsKeyboard(): boolean
```
**Описание:** Проверка на клавиатуру/мышь (cached state)
**Возвращает:** True для KeyboardAndMouse устройств
**Performance:** <0.001ms (direct boolean comparison)
**Use case:** UI hints, input prompts
#### IsGamepad()
```typescript
IsGamepad(): boolean
```
**Описание:** Проверка на геймпад/контроллер (cached state)
**Возвращает:** True для Gamepad устройств
**Performance:** <0.001ms (direct enum comparison)
**Use case:** UI hints, control schemes
#### GetCurrentInputDevice()
```typescript
GetCurrentInputDevice(): EHardwareDevicePrimaryType
```
**Описание:** Доступ к полному device type (cached state)
**Возвращает:** Native UE enum для device type
**Use case:** Debug information, detailed device classification
### Управление lifecycle
#### CleanupDeviceDetection()
```typescript
CleanupDeviceDetection(): void
```
**Описание:** Очистка системы и отвязка delegates
**Когда вызывать:** При уничтожении компонента
**Эффекты:** UnbindEvent, reset initialization state
### Testing и debug
#### ForceDeviceDetection()
```typescript
ForceDeviceDetection(): void
```
**Описание:** Принудительная повторная детекция устройства
**Use case:** Testing, debugging device state
## Система тестирования
### FT_InputDeviceDetection (Basic Functionality)
**Покрывает:**
- Успешность инициализации (`IsInitialized = true`)
- Корректность device queries (`IsKeyboard()` XOR `IsGamepad()`)
- Консистентность cached state с actual device
- Initial device detection работает
### FT_InputDeviceEvents (Event Handling)
**Покрывает:**
- Event binding и registration
- Manual event triggering через `ExecuteIfBound()`
- Device state transitions при events
- Event handling без errors
### FT_InputDeviceDebouncing (Performance)
**Покрывает:**
- Rapid event filtering (10 events → ≤1 change)
- Cooldown timing accuracy
- No memory leaks при intensive events
- Performance под нагрузкой
### Test Coverage
```typescript
TestScenarios = [
'Инициализация с correct delegate binding',
'Initial device detection работает',
'IsKeyboard/IsGamepad consistency проверки',
'Manual event firing changes device state',
'Rapid events properly debounced',
'Cleanup properly unbinds delegates',
'Toast notifications при device changes',
'Performance при intensive event load'
]
```
## Интеграция с Main Character
### Blueprint Integration
```typescript
// В BP_MainCharacter EventBeginPlay:
EventBeginPlay() →
Initialize Toast System →
Initialize Input Device Detection →
Initialize Other Systems...
// В custom events для UI updates:
OnNeedUIUpdate() →
Get Input Device Component → IsGamepad() →
Branch: Update UI Prompts accordingly
```
### Component References
```typescript
// В BP_MainCharacter variables:
Components:
├─ Input Device Component (AC_InputDevice)
├─ Toast System Component (AC_ToastSystem)
├─ Debug HUD Component (AC_DebugHUD)
└─ Movement Component (AC_Movement)
```
## Файловая структура
```
Content/
├── Input/
│ ├── Components/
│ │ └── AC_InputDevice.ts # Main component
│ └── Tests/
│ ├── FT_InputDeviceDetection.ts # Basic functionality
│ ├── FT_InputDeviceEvents.ts # Event handling
│ └── FT_InputDeviceDebouncing.ts # Performance testing
├── UE/ (Native UE wrappers)
│ ├── InputDeviceSubsystem.ts # Event delegate wrapper
│ ├── HardwareDeviceIdentifier.ts # UE device info struct
│ └── EHardwareDevicePrimaryType.ts # UE device enum
├── Debug/
│ └── Components/AC_DebugHUD.ts # Integration for debug page
└── Blueprints/
└── BP_MainCharacter.ts # Main integration point
```
## Best Practices
### Использование в коде
```typescript
// ✅ Хорошо - simple binary checks
if (this.InputDeviceComponent.IsGamepad()) {
this.SetGamepadUI();
} else {
this.SetKeyboardUI();
}
// ✅ Хорошо - proper initialization order
EventBeginPlay() →
InitializeToastSystem() →
InitializeDeviceDetection() →
InitializeOtherSystems()
// ✅ Хорошо - cleanup в EndPlay
EventEndPlay() →
this.InputDeviceComponent.CleanupDeviceDetection()
// ❌ Плохо - checking device type каждый Tick
EventTick() →
this.InputDeviceComponent.IsGamepad() // Wasteful!
// ✅ Хорошо - cache result или use events
OnDeviceChanged() →
this.CachedIsGamepad = this.InputDeviceComponent.IsGamepad()
```
### Performance recommendations
- **Cache device checks** если нужно в hot paths
- **Use event-driven UI updates** вместо polling в Tick
- **Initialize early** в BeginPlay для immediate availability
- **Cleanup properly** для предотвращения delegate leaks
## Известные ограничения
### Текущие ограничения
1. **Binary classification only** - только Gamepad vs KeyboardMouse
2. **UE 5.3+ requirement** - OnInputHardwareDeviceChanged delegate
3. **Single device focus** - нет multi-user support
4. **Basic debouncing** - фиксированный 300ms cooldown
### Архитектурные решения
- **Event-driven tradeoff:** Зависимость от UE delegate system
- **Binary simplicity:** Covers 99% game use cases
- **Fixed debouncing:** Простота важнее configurability
- **Toast integration:** Debug notifications не essential для core functionality
### Известные edge cases
- **Device disconnection:** Может не trigger event немедленно
- **Multiple gamepads:** Нет differentiation между controller 1 vs 2
- **Specialized hardware:** Racing wheels, flight sticks = "keyboard"
## Планы развития (при необходимости)
### Stage 6+: Enhanced Input Integration
1. **Automatic Input Mapping Context switching** based на device type
2. **Device-specific action bindings** (разные кнопки для разных геймпадов)
3. **Multi-user device tracking** для split-screen scenarios
### Долгосрочные улучшения
1. **Configurable debouncing** через Project Settings
2. **Device-specific sub-classification** (Xbox vs PlayStation controllers)
3. **Device capability queries** (rumble support, gyro, etc.)
4. **Cross-platform consistency** improvements
### Принцип расширения
- **Preserve binary simplicity** как primary API
- **Add specialized methods** для advanced use cases
- **Maintain event-driven approach** для consistency
- **Keep zero polling overhead** для performance
## Заключение
Input Device Detection System представляет собой event-driven обертку над Unreal Engine InputDeviceSubsystem, обеспечивающую простую бинарную классификацию устройств ввода с automatic debouncing и zero polling overhead.
**Ключевые достижения:**
- ✅ **Event-driven architecture:** Zero overhead при отсутствии device switching
- ✅ **Automatic debouncing:** Built-in защита от flickering и rapid switching
- ✅ **Binary simplicity:** IsGamepad() vs IsKeyboard() покрывает 99% use cases
- ✅ **UE 5.3+ integration:** Использование latest InputDeviceSubsystem features
- ✅ **Production ready:** Comprehensive testing и clean integration points
- ✅ **Toast integration:** Debug notifications для development convenience
**Архитектурные преимущества:**
- Event-driven design eliminates polling overhead completely
- Cached state обеспечивает instant access к device information
- Automatic debouncing решает stick drift и hardware timing issues
- Clean integration с existing Toast и Debug systems
- Ready для Enhanced Input integration в следующих этапах
**Performance characteristics:**
- Zero CPU overhead при отсутствии device switching
- <0.05ms processing time per device change event
- Instant device state queries через cached values
- Minimal memory footprint (~50 bytes total state)
Система готова к использованию в production и provides solid foundation для Enhanced Input integration в будущих этапах разработки.

View File

@ -1,133 +0,0 @@
// Input/Tests/FT_InputDeviceDetection.ts
import { AC_DebugHUD } from '#root/Debug/Components/AC_DebugHUD.ts';
import { AC_InputDevice } from '#root/Input/Components/AC_InputDevice.ts';
import { AC_ToastSystem } from '#root/Toasts/Components/AC_ToastSystem.ts';
import { EFunctionalTestResult } from '#root/UE/EFunctionalTestResult.ts';
import { EHardwareDevicePrimaryType } from '#root/UE/EHardwareDevicePrimaryType.ts';
import { FunctionalTest } from '#root/UE/FunctionalTest.ts';
/**
* Functional Test: Input Device Detection System
* Tests event-driven device detection with minimal wrapper approach
* Validates initialization, device queries, and delegate events
*/
export class FT_InputDeviceDetection extends FunctionalTest {
// ════════════════════════════════════════════════════════════════════════════════════════
// GRAPHS
// ════════════════════════════════════════════════════════════════════════════════════════
// ────────────────────────────────────────────────────────────────────────────────────────
// EventGraph
// ────────────────────────────────────────────────────────────────────────────────────────
/**
* Test entry point - validates complete device detection workflow
* Tests initialization, device queries, and simulated device changes
*/
EventStartTest(): void {
// Initialize components
this.ToastSystemComponent.InitializeToastSystem();
this.InputDeviceComponent.InitializeDeviceDetection(
this.ToastSystemComponent,
this.DebugHUDComponent
);
this.TestInitialization();
this.TestDeviceQueries();
this.FinishTest(EFunctionalTestResult.Succeeded);
}
// ════════════════════════════════════════════════════════════════════════════════════════
// TEST METHODS
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Test initialization and initial device detection
* @returns True if test passed
*/
private TestInitialization(): void {
// Validate initialization
if (this.InputDeviceComponent.IsInitialized) {
if (
this.InputDeviceComponent.GetCurrentInputDevice() !==
EHardwareDevicePrimaryType.Unspecified
) {
// Test passed
} else {
this.FinishTest(
EFunctionalTestResult.Failed,
'No initial device detected'
);
}
} else {
this.FinishTest(
EFunctionalTestResult.Failed,
'Input Device Detection failed to initialize'
);
}
}
/**
* Test device query functions consistency
* @returns True if test passed
*/
private TestDeviceQueries(): void {
const currentDevice = this.InputDeviceComponent.GetCurrentInputDevice();
const isKeyboard = this.InputDeviceComponent.IsKeyboard();
const isGamepad = this.InputDeviceComponent.IsGamepad();
// Validate that exactly one device type is active
if (!(isKeyboard && isGamepad)) {
if (isKeyboard || isGamepad) {
const expectedIsKeyboard =
currentDevice === EHardwareDevicePrimaryType.KeyboardAndMouse;
const expectedIsGamepad =
currentDevice === EHardwareDevicePrimaryType.Gamepad;
if (
isKeyboard === expectedIsKeyboard &&
isGamepad === expectedIsGamepad
) {
// Test passed
} else {
this.FinishTest(
EFunctionalTestResult.Failed,
'Device query functions inconsistent with GetCurrentInputDevice()'
);
}
} else {
this.FinishTest(
EFunctionalTestResult.Failed,
'Neither keyboard nor gamepad detected'
);
}
} else {
this.FinishTest(
EFunctionalTestResult.Failed,
'Both keyboard and gamepad detected simultaneously'
);
}
}
// ════════════════════════════════════════════════════════════════════════════════════════
// VARIABLES
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Input device detection system - component under test
* @category Components
*/
private InputDeviceComponent = new AC_InputDevice();
/**
* Toast notification system - required for device detection initialization
* @category Components
*/
private ToastSystemComponent = new AC_ToastSystem();
/**
* Debug HUD system - displays test status and parameters
* @category Components
*/
private DebugHUDComponent = new AC_DebugHUD();
}

Binary file not shown.

View File

@ -1,68 +1,5 @@
// Levels/TestLevel.ts
// Content/Levels/TestLevel.ts
import { BP_MainCharacter } from '#root/Blueprints/BP_MainCharacter.ts';
import { FT_CameraInitialization } from '#root/Camera/Tests/FT_CameraInitialization.ts';
import { FT_CameraLimits } from '#root/Camera/Tests/FT_CameraLimits.ts';
import { FT_CameraRotation } from '#root/Camera/Tests/FT_CameraRotation.ts';
import { FT_CameraSensitivity } from '#root/Camera/Tests/FT_CameraSensitivity.ts';
import { FT_CameraSmoothing } from '#root/Camera/Tests/FT_CameraSmoothing.ts';
import { FT_DebugNavigation } from '#root/Debug/Tests/FT_DebugNavigation.ts';
import { FT_DebugPageManagement } from '#root/Debug/Tests/FT_DebugPageManagement.ts';
import { FT_DebugSystem } from '#root/Debug/Tests/FT_DebugSystem.ts';
import { FT_InputDeviceDetection } from '#root/Input/Tests/FT_InputDeviceDetection.ts';
import { FT_BasicMovement } from '#root/Movement/Tests/FT_BasicMovement.ts';
import { FT_SurfaceClassification } from '#root/Movement/Tests/FT_SurfaceClassification.ts';
import { FT_ToastLimit } from '#root/Toasts/Tests/FT_ToastLimit.ts';
import { FT_ToastsDurationHandling } from '#root/Toasts/Tests/FT_ToastsDurationHandling.ts';
import { FT_ToastsEdgeCases } from '#root/Toasts/Tests/FT_ToastsEdgeCases.ts';
import { FT_ToastsSystemInitialization } from '#root/Toasts/Tests/FT_ToastsSystemInitialization.ts';
import { FT_ToastsToastCreation } from '#root/Toasts/Tests/FT_ToastsToastCreation.ts';
import { BP_MainCharacter } from '/Content/Blueprints/BP_MainCharacter.ts';
new BP_MainCharacter();
// Camera Tests
const CameraInitializationTest = new FT_CameraInitialization();
const CameraLimitsTest = new FT_CameraLimits();
const CameraRotationTest = new FT_CameraRotation();
const CameraSensitivityTest = new FT_CameraSensitivity();
const CameraSmoothingTest = new FT_CameraSmoothing();
CameraInitializationTest.EventStartTest();
CameraLimitsTest.EventStartTest();
CameraRotationTest.EventStartTest();
CameraSensitivityTest.EventStartTest();
CameraSmoothingTest.EventStartTest();
// Debug Tests
const DebugNavigationTest = new FT_DebugNavigation();
const DebugSystemTest = new FT_DebugSystem();
const DebugPageManagementTest = new FT_DebugPageManagement();
DebugNavigationTest.EventStartTest();
DebugSystemTest.EventStartTest();
// Input Tests
const InputDeviceDetectionTest = new FT_InputDeviceDetection();
InputDeviceDetectionTest.EventStartTest();
// Movement Tests
const BasicMovementTest = new FT_BasicMovement();
const SurfaceClassificationTest = new FT_SurfaceClassification();
BasicMovementTest.EventStartTest();
SurfaceClassificationTest.EventStartTest();
// Toasts Tests
const ToastLimitsTest = new FT_ToastLimit();
const ToastsDurationHandlingTest = new FT_ToastsDurationHandling();
const ToastsEdgeCasesTest = new FT_ToastsEdgeCases();
const ToastsSystemInitializationTest = new FT_ToastsSystemInitialization();
const ToastsToastCreationTest = new FT_ToastsToastCreation();
ToastLimitsTest.EventStartTest();
ToastsDurationHandlingTest.EventStartTest();
ToastsEdgeCasesTest.EventStartTest();
ToastsSystemInitializationTest.EventStartTest();
ToastsToastCreationTest.EventStartTest();
DebugPageManagementTest.EventStartTest();

BIN
Content/Levels/TestLevel.umap (Stored with Git LFS)

Binary file not shown.

View File

@ -1,85 +0,0 @@
// Math/Libraries/BFL_Vectors.ts
import { BlueprintFunctionLibrary } from '#root/UE/BlueprintFunctionLibrary.ts';
import type { Float } from '#root/UE/Float.ts';
import { MathLibrary } from '#root/UE/MathLibrary.ts';
import { Vector } from '#root/UE/Vector.ts';
/**
* Blueprint Function Library: Vector Mathematics
* Pure mathematical functions for vector operations and surface angle calculations
* Used by movement system for deterministic surface classification
*/
export class BFL_VectorsClass extends BlueprintFunctionLibrary {
constructor(
outer: null | BlueprintFunctionLibrary = null,
name: string = 'BFL_Vectors'
) {
super(outer, name);
}
// ════════════════════════════════════════════════════════════════════════════════════════
// FUNCTIONS
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Calculate angle between two normalized vectors
* @param Vector1 - First normalized vector
* @param Vector2 - Second normalized vector
* @returns Angle between vectors in radians (0 to π)
* @example
* // 90° angle between X and Z axes
* GetAngleBetweenVectors(new Vector(1,0,0), new Vector(0,0,1)) // returns π/2
*/
public GetAngleBetweenVectors(Vector1: Vector, Vector2: Vector): Float {
/**
* Internal calculation using dot product and arccosine
*/
const CalculateAngleBetweenVectors = (v1: Vector, v2: Vector): Float =>
MathLibrary.Acos(MathLibrary.Dot(v1, v2));
return CalculateAngleBetweenVectors(Vector1, Vector2);
}
/**
* Generate surface normal vector from angle in degrees
* @param AngleDegrees - Angle from horizontal in degrees (0-180)
* @returns Normalized surface normal vector
* @example
* // Flat surface (0°)
* GetNormalFromAngle(0) // returns Vector(0,0,1)
* // Vertical wall (90°)
* GetNormalFromAngle(90) // returns Vector(1,0,0)
*/
public GetNormalFromAngle(AngleDegrees: Float): Vector {
/**
* Calculate X component using sine of angle
*/
const CalculateX = (angle: Float): Float =>
MathLibrary.Sin(MathLibrary.DegreesToRadians(angle));
/**
* Calculate Z component using cosine of angle
*/
const CalculateZ = (angle: Float): Float =>
MathLibrary.Cos(MathLibrary.DegreesToRadians(angle));
return new Vector(CalculateX(AngleDegrees), 0, CalculateZ(AngleDegrees));
}
/**
* Calculate angle between surface normal and up vector
* @param SurfaceNormal - Normalized surface normal vector
* @returns Angle from horizontal plane in radians (0 = flat, π/2 = vertical)
* @example
* // Flat surface
* GetSurfaceAngle(new Vector(0,0,1)) // returns 0
* // Vertical wall
* GetSurfaceAngle(new Vector(1,0,0)) // returns π/2
*/
public GetSurfaceAngle(SurfaceNormal: Vector): Float {
return this.GetAngleBetweenVectors(SurfaceNormal, new Vector(0, 0, 1));
}
}
export const BFL_Vectors = new BFL_VectorsClass();

BIN
Content/Math/Libraries/BFL_Vectors.uasset (Stored with Git LFS)

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@ -0,0 +1,25 @@
// Content/Movement/Core/DA_TengriMovementConfig.ts
import { TengriMovementConfig } from '/Source/TengriPlatformer/Movement/Core/TengriMovementConfig.ts';
export class DA_TengriMovementConfig extends TengriMovementConfig {
override MaxSpeed = 800.0;
override Acceleration = 2048.0;
override Friction = 8.0;
override Gravity = 980.0;
override RotationSpeed = 360.0;
override MinSpeedForRotation = 10.0;
override SteepSlopeSlideFactor = 0.0;
override CapsuleRadius = 34.0;
override CapsuleHalfHeight = 88.0;
override MaxSlideIterations = 3;
override MaxStepHeight = 45.0;
override GroundSnapDistance = 20.0;
override GroundSnapOffset = 0.15;
override WalkableAngleDeg = 50.0;
override SteepSlopeAngleDeg = 85.0;
override WallAngleDeg = 95.0;
}

BIN
Content/Movement/DA_TengriMovementConfig.uasset (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -1,7 +0,0 @@
// Movement/Enums/E_MovementState.ts
export enum E_MovementState {
Idle = 'Idle',
Walking = 'Walking',
Airborne = 'Airborne',
}

Binary file not shown.

View File

@ -1,9 +0,0 @@
// Movement/Enums/E_SurfaceType.ts
export enum E_SurfaceType {
None = 'None',
Walkable = 'Walkable',
SteepSlope = 'SteepSlope',
Wall = 'Wall',
Ceiling = 'Ceiling',
}

Binary file not shown.

View File

@ -1,236 +0,0 @@
[//]: # (Movement/ManualTestingChecklist.md)
# Movement System - Manual Testing Checklist
## Тестовая среда
- **Уровень:** TestLevel с BP_MainCharacter
- **Требования:** MovementComponent инициализирован
- **Debug HUD:** Включен для проверки параметров
---
## 1. Инициализация системы
### 1.1 Базовая инициализация
- [ ] **InitializeMovementSystem()** выполняется без ошибок при запуске уровня
- [ ] **IsInitialized flag** устанавливается в true после инициализации
- [ ] **Angle conversion** - пороги корректно конвертируются из градусов в радианы
- [ ] **CapsuleComponent reference** - передаётся и сохраняется корректно (этап 9)
---
## 2. Константы движения
### 2.1 Default значения
- [ ] **MaxSpeed = 800.0** - значение установлено по умолчанию
- [ ] **Acceleration = 10.0** - значение установлено по умолчанию
- [ ] **Friction = 8.0** - значение установлено по умолчанию
- [ ] **Gravity = 980.0** - значение установлено по умолчанию
### 2.2 Пороговые углы
- [ ] **Walkable = 50.0°** - значение по умолчанию в градусах
- [ ] **SteepSlope = 85.0°** - значение по умолчанию в градусах
- [ ] **Wall = 95.0°** - значение по умолчанию в градусах
### 2.3 Sweep Collision константы (Этап 9)
- [ ] **MaxStepSize = 50.0** - максимальный размер шага sweep
- [ ] **MinStepSize = 1.0** - минимальный размер шага
- [ ] **MaxCollisionChecks = 25** - лимит проверок за кадр
- [ ] **GroundTraceDistance = 5.0** - дистанция trace вниз для ground detection
---
## 3. Базовое движение (Этап 7)
### 3.1 Управление клавиатурой
- [ ] **W** - персонаж движется вперед (+X направление)
- [ ] **S** - персонаж движется назад (-X направление)
- [ ] **A** - персонаж движется влево (+Y направление)
- [ ] **D** - персонаж движется вправо (-Y направление)
- [ ] **Отсутствие input** - персонаж останавливается
### 3.2 Управление геймпадом
- [ ] **Left Stick Up** - движение вперед
- [ ] **Left Stick Down** - движение назад
- [ ] **Left Stick Left** - движение влево
- [ ] **Left Stick Right** - движение вправо
- [ ] **Stick в центре** - персонаж останавливается
### 3.3 Физика движения
- [ ] **Плавное ускорение** - персонаж набирает скорость постепенно при нажатии клавиш
- [ ] **Плавное торможение** - персонаж останавливается плавно при отпускании клавиш
- [ ] **MaxSpeed limit** - скорость не превышает 800.0 units/sec
- [ ] **Диагональное движение** - скорость диагонального движения равна прямому (не быстрее)
- [ ] **Стабильное поведение** - нет рывков, заиканий или неожиданных ускорений
### 3.4 Состояния движения
- [ ] **Idle state** - MovementState = Idle когда персонаж стоит
- [ ] **Walking state** - MovementState = Walking при движении
- [ ] **Airborne state** - MovementState = Airborne в воздухе (этап 9)
- [ ] **InputMagnitude** - корректно отражает силу input (0-1)
- [ ] **CurrentSpeed** - показывает текущую горизонтальную скорость
---
## 4. Ground Detection и Падение (Этап 9)
### 4.1 Базовое падение и приземление
- [ ] **Падение начинается:** Персонаж падает вниз с нормальной скоростью
- [ ] **Приземление без провалов:** Персонаж останавливается НА полу, а не В полу
- [ ] **Стабильная Z позиция:** После приземления Z координата стабильна (±0.5 единиц)
- [ ] **IsGrounded = true:** Debug HUD показывает `Is Grounded: true` после приземления
- [ ] **Velocity.Z = 0:** После приземления вертикальная скорость обнулена
**Ожидаемые значения в Debug HUD:**
```
Current Velocity: X=0.00 Y=0.00 Z=0.00
Is Grounded: true
Location Z: ~0.125 (стабильно)
```
### 4.2 Движение по полу без провалов
- [ ] **Движение WASD:** Персонаж двигается по полу плавно
- [ ] **Нет дёрганий Z:** При движении нет вертикальных рывков
- [ ] **Z позиция стабильна:** Разброс Z ≤ 0.5 единиц во время движения
- [ ] **Collision Checks:** В Debug HUD не превышает 25
**Ожидаемые значения в Debug HUD:**
```
Speed: 600-800
Is Grounded: true
Collision Checks: 3-8/25
```
### 4.3 Край платформы
- [ ] **Подход к краю:** Персонаж может подойти к краю платформы
- [ ] **Схождение с края:** Персонаж начинает падать после выхода за край
- [ ] **IsGrounded = false:** Debug HUD показывает airborne state
- [ ] **Короткая "липкость":** Капсула может кратковременно зацепиться (это нормально)
- [ ] **Повторное приземление:** После падения с края может приземлиться снова
**Известное поведение:** Лёгкое "прилипание" к краю из-за скруглённой капсулы - это нормально, исправим в этапе 15
---
## 5. Sweep Collision Performance (Этап 9)
### 5.1 Количество collision checks
| Сценарий | Ожидаемое кол-во checks |
|----------|------------------------|
| Стоит на месте | 0-1 |
| Медленное движение | 2-5 |
| Нормальная скорость | 5-12 |
| Максимальная скорость | 15-25 |
| Падение с высоты | 10-20 |
- [ ] **Idle:** Collision Checks = 0-1
- [ ] **Walking:** Collision Checks = 5-12
- [ ] **Fast movement:** Не превышает MaxCollisionChecks (25)
### 5.2 Адаптивный размер шага
- [ ] **При медленном движении:** Меньше traces (видно в visual debug)
- [ ] **При быстром движении:** Больше traces, меньше расстояние между ними
- [ ] **Падение:** Частые проверки во время быстрого падения
**Visual debug traces должны показать:** Короткие шаги при высокой скорости, длинные при низкой
---
## 6. Детерминированность (Этап 9)
### 6.1 Тест повторяемости
**Процедура:**
1. Запомнить начальную позицию персонажа
2. Подвигать персонажа в определённом направлении 5 секунд
3. Перезапустить уровень
4. Повторить те же движения
5. Сравнить финальные позиции
**Проверки:**
- [ ] **Z координата идентична:** Разница ≤ 0.5 единиц
- [ ] **XY координаты близки:** Небольшое отклонение допустимо (инпут timing)
- [ ] **IsGrounded одинаков:** Один и тот же state в конце
---
## 7. Debug HUD Integration
### 7.1 Movement Info Page
- [ ] **Константы** отображаются корректно:
- Max Speed: 800
- Acceleration: 10
- Friction: 8
- Gravity: 980
- Initialized: true
- [ ] **Текущее состояние** отображается:
- Current Velocity: X, Y, Z компоненты
- Speed: горизонтальная скорость
- Is Grounded: true/false
- Surface Type: Walkable (пока всегда)
- Movement State: Idle/Walking/Airborne
- Input Magnitude: 0.00-1.00
- [ ] **Rotation info** (этап 8):
- Current Yaw
- Target Yaw
- Rotation Delta
- Is Rotating
- [ ] **Position** (этап 9):
- Location: X, Y, Z координаты
- [ ] **Sweep Collision** (этап 9):
- Collision Checks: X/25
- Ground Distance: 5.0 cm
### 7.2 Реальное время обновления
- [ ] **Velocity** изменяется в реальном времени при движении
- [ ] **Speed** корректно показывает magnitude горизонтальной скорости
- [ ] **Movement State** переключается между Idle/Walking/Airborne
- [ ] **Input Magnitude** отражает силу нажатия
- [ ] **Collision Checks** обновляется каждый кадр при движении
- [ ] **Location** обновляется плавно
---
## 8. Автотесты Integration
### 8.1 FT_SurfaceClassification
- [ ] **Тест проходит** - классификация поверхностей по углам
### 8.2 FT_MovementInitialization
- [ ] **Тест проходит** - инициализация, начальные состояния, конфигурация
### 8.3 Удалённые тесты
- ❌ **FT_BasicMovement** - удалён (требует тестовый уровень)
- ❌ **FT_DiagonalMovement** - удалён (требует тестовый уровень)
---
## 9. Performance
### 9.1 Производительность
- [ ] **Stable 60+ FPS** при активном движении
- [ ] **No memory leaks** при длительном использовании
- [ ] **Smooth movement** без микро-заиканий
- [ ] **Sweep overhead** минимален (<1ms дополнительно)
### 9.2 Отзывчивость
- [ ] **Instant response** на нажатие клавиш (нет input lag)
- [ ] **Smooth transitions** между состояниями движения
- [ ] **Consistent timing** независимо от FPS
---
## Критерии прохождения этапов
### Этап 7: Базовое движение
- [ ] Все основные направления движения работают
- [ ] Физика движения плавная и отзывчивая
- [ ] MaxSpeed limit соблюдается
- [ ] Диагональное движение не дает преимущества в скорости
### Этап 9: Sweep Collision + Ground Detection
- [ ] Полное отсутствие tunneling при любых скоростях
- [ ] Стабильная Z позиция (разброс <0.5 единиц)
- [ ] Детерминированность (100% воспроизводимость)
- [ ] Performance <25 collision checks за кадр
- [ ] Значения корректно отображаются в Debug HUD

View File

@ -1,9 +0,0 @@
// Movement/Structs/S_AngleThresholds.ts
import type { Float } from '#root/UE/Float.ts';
export interface S_AngleThresholds {
Walkable: Float;
SteepSlope: Float;
Wall: Float;
}

Binary file not shown.

View File

@ -1,10 +0,0 @@
// Movement/Structs/S_SurfaceTestCase.ts
import type { E_SurfaceType } from '#root/Movement/Enums/E_SurfaceType.ts';
import type { Float } from '#root/UE/Float.ts';
export interface S_SurfaceTestCase {
AngleDegrees: Float;
ExpectedType: E_SurfaceType;
Description: string;
}

Binary file not shown.

View File

@ -1,690 +0,0 @@
[//]: # (Movement/TDD.md)
# Movement System - Technical Documentation
## Обзор
Детерминированная система движения для 3D-платформера с точной классификацией поверхностей, swept collision detection и ground detection. Система обеспечивает математически предсказуемое поведение для физики движения персонажа с плавным ускорением, торможением и защитой от tunneling.
## Архитектурные принципы
- **Детерминизм:** Математически предсказуемые результаты для одинаковых входных данных
- **Инкапсуляция:** Приватные константы с публичным API доступом через геттеры
- **Производительность:** Прямой доступ к полям класса без промежуточных структур
- **Модульность:** Система классификации поверхностей отделена от физики движения
- **Тестируемость:** Полное покрытие через публичные геттеры и testing interface
## Компоненты системы
### AC_Movement (Core Component)
**Ответственности:**
- Классификация поверхностей по углу наклона
- Управление движковыми константами
- Обработка input и расчет velocity
- Swept collision detection для предотвращения tunneling
- Ground detection и snapping
- Gravity и friction применение
- Character rotation управление
**Ключевые функции:**
**Инициализация:**
- `InitializeMovementSystem()` - Инициализация с конвертацией углов и компонент setup
**Surface Classification:**
- `ClassifySurface()` - Определение типа поверхности по normal вектору
- Приватные методы проверки: `IsSurfaceWalkable()`, `IsSurfaceSteep()`, `IsSurfaceWall()`, `IsSurfaceCeiling()`, `IsSurfaceNone()`
**Movement Processing:**
- `ProcessMovementInput()` - Главная точка входа для обработки движения
- `ProcessGroundMovement()` - VInterpTo physics для плавного движения по земле
- `ApplyFriction()` - Система торможения через VInterpTo
- `ApplyGravity()` - Вертикальная физика для airborne состояний
- `UpdateMovementState()` - Определение текущего состояния (Idle/Walking/Airborne)
- `UpdateCurrentSpeed()` - Расчет горизонтальной скорости
**Collision System:**
- `PerformDeterministicSweep()` - Stepped sweep для предотвращения tunneling
- `HandleSweepCollision()` - Slide response по поверхности коллизии
- `CalculateAdaptiveStepSize()` - Динамический размер шага based on velocity
- `ResetCollisionCounter()` - Сброс счетчика коллизий каждый кадр
**Ground Detection:**
- `CheckGround()` - Line trace для определения walkable ground
- Ground snapping logic в `ApplyMovementWithSweep()`
**Character Rotation:**
- `CalculateTargetRotation()` - Определение целевого yaw based on input
- `UpdateCharacterRotation()` - Плавная интерполяция к target rotation
**Public API (Getters):**
- `GetMaxSpeed()` - Максимальная горизонтальная скорость
- `GetCurrentVelocity()` - Текущий velocity вектор
- `GetMovementState()` - Текущее состояние движения
- `GetCurrentSpeed()` - Текущая горизонтальная скорость
- `GetCurrentRotation()` - Текущий rotation персонажа
- `GetIsInitialized()` - Флаг успешной инициализации
**Debug:**
- `UpdateDebugPage()` - Обновление Debug HUD с movement info
### BFL_Vectors (Blueprint Function Library)
**Ответственности:**
- Чистые математические функции для работы с векторами
- Расчет углов между векторами
- Генерация surface normal из угла
- Вычисление угла поверхности
**Ключевые функции:**
- `GetAngleBetweenVectors()` - Угол между двумя нормализованными векторами
- `GetNormalFromAngle()` - Создание normal вектора из угла в градусах
- `GetSurfaceAngle()` - Угол поверхности от горизонтальной плоскости
## Классификация поверхностей
### Типы поверхностей (E_SurfaceType)
```typescript
enum E_SurfaceType {
None = 'None', // Отсутствие контакта (полет)
Walkable = 'Walkable', // Обычное движение ≤50°
SteepSlope = 'SteepSlope', // Скольжение 50°-85°
Wall = 'Wall', // Блокировка 85°-95°
Ceiling = 'Ceiling' // Потолок >95°
}
```
### Пороговые значения углов
```typescript
AngleThresholdsDegrees: S_AngleThresholds = {
Walkable: 50.0, // Максимальный угол для ходьбы
SteepSlope: 85.0, // Максимальный угол для скольжения
Wall: 95.0 // Максимальный угол для стены
}
```
### Логика классификации
```
Угол поверхности → Тип поверхности
0° - 50° → Walkable (нормальная ходьба)
50° - 85° → SteepSlope (скольжение вниз)
85° - 95° → Wall (блокировка движения)
95° - 180° → Ceiling (потолочная поверхность)
```
## Структуры данных
### Movement Configuration
**Приватные константы движения:**
```typescript
private readonly MaxSpeed: Float = 800.0; // Max horizontal speed
private readonly Acceleration: Float = 10.0; // VInterpTo speed for acceleration
private readonly Friction: Float = 8.0; // VInterpTo speed for deceleration
private readonly Gravity: Float = 980.0; // Vertical acceleration when airborne
```
**Доступ к константам:**
```typescript
// Internal use (direct access within AC_Movement)
this.CurrentVelocity.X * this.MaxSpeed
// External use (via public getters)
const maxSpeed = this.MovementComponent.GetMaxSpeed();
```
**Character Rotation Config:**
```typescript
private readonly RotationSpeed: Float = 720.0; // Degrees per second
private readonly ShouldRotateToMovement: boolean = true; // Enable/disable rotation
private readonly MinSpeedForRotation: Float = 50.0; // Min speed threshold
```
**Collision Config:**
```typescript
private readonly MaxStepSize: Float = 50.0; // Max sweep step size
private readonly MinStepSize: Float = 1.0; // Min sweep step size
private readonly MaxCollisionChecks: number = 25; // Max checks per frame
```
**Ground Detection Config:**
```typescript
private readonly GroundTraceDistance: Float = 5.0; // Downward trace distance
```
### S_AngleThresholds
```typescript
interface S_AngleThresholds {
Walkable: Float // Порог walkable поверхности
SteepSlope: Float // Порог steep slope поверхности
Wall: Float // Порог wall поверхности
}
```
### S_SurfaceTestCase (для тестирования)
```typescript
interface S_SurfaceTestCase {
AngleDegrees: Float // Угол в градусах для теста
ExpectedType: E_SurfaceType // Ожидаемый результат классификации
Description: string // Описание тестового случая
}
```
## Физика движения
### VInterpTo Movement System
Основная логика движения использует VInterpTo для плавного ускорения и торможения.
**Acceleration flow:**
```typescript
ProcessGroundMovement(InputVector, DeltaTime) →
CalculateTargetVelocity(InputVector, MaxSpeed) →
VInterpTo(CurrentVelocity, TargetVelocity, DeltaTime, Acceleration)
```
**Friction flow:**
```typescript
ApplyFriction(DeltaTime) →
VInterpTo(CurrentVelocity, ZeroVelocity, DeltaTime, Friction)
```
**Gravity application:**
```typescript
ApplyGravity() →
if (!IsGrounded) velocity.Z -= Gravity
else velocity.Z = 0
```
### Swept Collision Detection
**Adaptive stepping:**
```typescript
CalculateAdaptiveStepSize(Velocity, DeltaTime) →
frameDistance = VectorLength(Velocity.XY) * DeltaTime
if frameDistance < MinStepSize: return MaxStepSize
else: return Clamp(frameDistance * 0.5, MinStepSize, MaxStepSize)
```
**Deterministic sweep:**
```typescript
PerformDeterministicSweep(StartLocation, DesiredDelta, DeltaTime) →
stepSize = CalculateAdaptiveStepSize()
numSteps = Min(Ceil(totalDistance / stepSize), MaxCollisionChecks)
for each step:
CapsuleTraceByChannel() → if hit: return HitResult
return final location
```
**Collision response:**
```typescript
HandleSweepCollision(HitResult, RemainingDelta) →
slideVector = RemainingDelta - Dot(HitNormal, RemainingDelta) * HitNormal
return slideVector
```
### Ground Detection & Snapping
**Ground check:**
```typescript
CheckGround() →
startZ = ActorLocation.Z - CapsuleHalfHeight
endZ = startZ - GroundTraceDistance
LineTraceByChannel() → if hit and walkable: return HitResult
```
**Ground snapping:**
```typescript
if IsGrounded and LastGroundHit.BlockingHit and velocity.Z <= 0:
correctZ = LastGroundHit.Location.Z + CapsuleHalfHeight
if abs(currentZ - correctZ) within snap range:
SetActorLocation(x, y, correctZ)
```
### E_MovementState (Movement States)
- **Idle:** Персонаж стоит на месте (IsGrounded && InputMagnitude < 0.01)
- **Walking:** Движение по земле (IsGrounded && InputMagnitude > 0.01)
- **Airborne:** В воздухе (!IsGrounded)
### Input Processing Chain
```
Enhanced Input →
BP_MainCharacter.EnhancedInputActionMoveTriggered() →
Calculate camera-relative input →
AC_Movement.ProcessMovementInput() →
ProcessGroundMovement() / ApplyFriction() →
ApplyGravity() →
ApplyMovementWithSweep() →
Character moves with collision protection
```
## Математическая основа
### Расчет угла поверхности
```typescript
// 1. Получение угла между surface normal и up vector (0,0,1)
const surfaceAngle = GetAngleBetweenVectors(surfaceNormal, Vector(0,0,1))
// 2. Использование dot product и arccosine
const dotProduct = Dot(vector1, vector2)
const angle = Acos(dotProduct) // результат в радианах
// 3. Классификация по пороговым значениям в радианах
if (surfaceAngle <= thresholds.Walkable) return E_SurfaceType.Walkable
```
### Генерация test normal vectors
```typescript
GetNormalFromAngle(angleDegrees: Float): Vector {
const x = Sin(DegreesToRadians(angleDegrees)) // горизонтальная компонента
const z = Cos(DegreesToRadians(angleDegrees)) // вертикальная компонента
return new Vector(x, 0, z) // нормализованный вектор
}
```
### Rotation calculation
```typescript
CalculateTargetRotation(MovementDirection: Vector): Rotator {
targetYaw = RadiansToDegrees(Atan2(direction.Y, direction.X))
return new Rotator(0, targetYaw, 0) // pitch=0, roll=0
}
```
## Производительность
### Оптимизации
- **Прямой доступ к полям:** Без промежуточных структур
- **Кэширование радиан:** Конвертация градусы→радианы только при инициализации
- **Adaptive stepping:** Меньше collision checks при медленном движении
- **Раннее возвращение:** Немедленный return при hit detection
- **Чистые функции:** Все математические операции без side effects
### Benchmarks
- **Инициализация:** <0.1ms (конвертация 3 углов + setup)
- **ClassifySurface:** <0.05ms на вызов
- **PerformDeterministicSweep:** 0.05-0.5ms (зависит от velocity)
- **CheckGround:** <0.02ms (single line trace)
- **ProcessMovementInput:** 0.1-0.7ms (полный frame processing)
- **Memory footprint:** ~300 байт на компонент
### Performance Metrics
**Collision checks per frame:**
| Сценарий | Checks |
|----------|--------|
| Idle | 0-1 |
| Slow walk | 2-5 |
| Normal speed | 5-12 |
| Max speed | 15-25 |
| Falling | 10-20 |
**Frame budget:** <1ms для всех movement operations
## Система тестирования
### Test Coverage Strategy
**Automated Tests:**
- ✅ 100% критических pure functions (surface classification, initialization)
- ✅ Граничные условия (пороговые углы)
- ⚠️ Частичное покрытие физики (без симуляции коллизий)
**Manual Testing:**
- 📝 Comprehensive checklist для collision и physics
- 📝 Determinism validation procedures
- 📝 Performance benchmarks
### FT_MovementConfiguration
**Покрывает:**
- Default значения движковых констант
- Валидация положительных значений
- Логические соотношения (Friction ≤ Acceleration)
**Assertions:**
```typescript
config = GetTestData()
AssertEqual(config.MaxSpeed, 800.0)
AssertEqual(config.Acceleration, 10.0)
AssertEqual(config.Friction, 8.0)
AssertEqual(config.Gravity, 980.0)
AssertTrue(config.Friction <= config.Acceleration)
```
### FT_BasicMovement
**Покрывает:**
- Успешность инициализации
- Начальное состояние (Idle, zero velocity)
- Public API availability
**Test Flow:**
```typescript
1. InitializeMovementSystem()
✅ GetIsInitialized() returns true
2. Initial State Check
✅ GetMovementState() === E_MovementState.Idle
3. Initial Velocity Check
✅ GetCurrentSpeed() === 0
✅ GetCurrentVelocity() === (0, 0, 0)
```
### FT_SurfaceClassification
**Покрывает:**
- Классификацию поверхностей по углам (10 тест кейсов)
- Граничные условия для всех типов поверхностей
- Экстремальные углы (0° - 180°)
**Test Cases:**
```typescript
[
{ 0° → Walkable } // Flat surface
{ 25° → Walkable } // Gentle slope
{ 49° → Walkable } // Max walkable (boundary)
{ 51° → SteepSlope } // Steep slope (boundary)
{ 70° → SteepSlope } // Very steep
{ 84° → SteepSlope } // Max steep (boundary)
{ 90° → Wall } // Vertical wall (boundary)
{ 94° → Wall } // Max wall (boundary)
{ 120° → Ceiling } // Overhang
{ 180° → Ceiling } // Ceiling
]
```
### Test Coverage Summary
| Категория | Автотесты | Manual | Coverage |
|-----------|-----------|--------|----------|
| Инициализация | ✅ FT_BasicMovement<br>✅ FT_MovementConfiguration | - | 100% |
| Surface Classification | ✅ FT_SurfaceClassification | - | 100% |
| Movement Constants | ✅ FT_MovementConfiguration | - | 100% |
| Basic Physics | ❌ | ✅ Manual | 0% auto / 100% manual |
| Sweep Collision | ❌ | ✅ Manual | 0% auto / 100% manual |
| Ground Detection | ❌ | ✅ Manual | 0% auto / 100% manual |
**Итого:** 3 automated test suites, ~15 assertions, 100% coverage критических функций
## Интеграция с системами
### Debug HUD Integration
```typescript
UpdateDebugPage(): void {
this.DebugHUDComponent.UpdatePageContent(
this.DebugPageID,
// Constants
`Max Speed: ${this.MaxSpeed}\n` +
`Acceleration: ${this.Acceleration}\n` +
`Friction: ${this.Friction}\n` +
`Gravity: ${this.Gravity}\n` +
// Current State
`Current Velocity: ${ConvVectorToString(this.CurrentVelocity)}\n` +
`Speed: ${this.CurrentSpeed}\n` +
`Is Grounded: ${this.IsGrounded}\n` +
`Movement State: ${this.MovementState}\n` +
// Rotation
`Current Yaw: ${this.CurrentRotation.yaw}°\n` +
`Target Yaw: ${this.TargetRotation.yaw}°\n` +
`Rotation Delta: ${this.RotationDelta}°\n` +
// Collision
`Collision Checks: ${this.SweepCollisionCount}/${this.MaxCollisionChecks}\n`
);
}
```
### Main Character Integration
```typescript
// BP_MainCharacter.ts EventBeginPlay
this.MovementComponent.InitializeMovementSystem(
this.CharacterCapsule,
this.DebugHUDComponent
);
// EventTick
this.MovementComponent.ProcessMovementInput(
this.CurrentMovementInput,
DeltaTime
);
this.SetActorRotation(
this.MovementComponent.GetCurrentRotation()
);
```
### Physics System Integration
- **Collision detection:** CapsuleTraceByChannel для swept movement
- **Ground detection:** LineTraceByChannel для ground check
- **Movement constraints:** Walkable surface detection блокирует non-walkable movement
- **Sliding mechanics:** Slide vector calculation для smooth collision response
## API Reference
### Public Methods
#### InitializeMovementSystem()
```typescript
InitializeMovementSystem(
CapsuleComponentRef: CapsuleComponent | null,
DebugHUDComponentRef: AC_DebugHUD | null
): void
```
**Описание:** Инициализирует систему движения
**Параметры:**
- `CapsuleComponentRef` - Capsule для collision detection
- `DebugHUDComponentRef` - Debug HUD для визуализации
**Эффекты:**
- Устанавливает IsInitialized = true
- Конвертирует пороги градусы → радианы
- Создает debug page если HUD предоставлен
#### ProcessMovementInput()
```typescript
ProcessMovementInput(InputVector: Vector, DeltaTime: Float): void
```
**Описание:** Главная точка входа для обработки движения каждый кадр
**Параметры:**
- `InputVector` - Camera-relative movement input
- `DeltaTime` - Frame delta time
**Flow:**
1. Calculate target rotation
2. Update character rotation
3. Check ground
4. Process ground movement OR apply friction
5. Apply gravity
6. Update movement state
7. Apply movement with sweep
#### ClassifySurface()
```typescript
ClassifySurface(SurfaceNormal: Vector): E_SurfaceType
```
**Параметры:** `SurfaceNormal` - Нормализованный вектор поверхности
**Возвращает:** Тип поверхности
**Требования:** Вектор должен быть нормализован
#### Public Getters
```typescript
GetMaxSpeed(): Float // Максимальная скорость
GetCurrentVelocity(): Vector // Текущий velocity
GetMovementState(): E_MovementState // Текущее состояние
GetCurrentSpeed(): Float // Текущая горизонтальная скорость
GetCurrentRotation(): Rotator // Текущий rotation
GetIsInitialized(): boolean // Флаг инициализации
```
### Configuration Properties
**Movement Constants (Instance Editable):**
```typescript
MaxSpeed: 800.0 // Units per second
Acceleration: 10.0 // VInterpTo speed
Friction: 8.0 // VInterpTo speed
Gravity: 980.0 // cm/s² (Earth gravity)
```
**Angle Thresholds (Instance Editable):**
```typescript
AngleThresholdsDegrees: {
Walkable: 50.0° // Max walkable angle
SteepSlope: 85.0° // Max steep slope angle
Wall: 95.0° // Max wall angle
}
```
**Rotation Config (Instance Editable):**
```typescript
RotationSpeed: 720.0 // Degrees per second
ShouldRotateToMovement: true // Enable rotation
MinSpeedForRotation: 50.0 // Min speed threshold
```
**Collision Config (Instance Editable):**
```typescript
MaxStepSize: 50.0 // Max sweep step
MinStepSize: 1.0 // Min sweep step
MaxCollisionChecks: 25 // Max checks per frame
GroundTraceDistance: 5.0 // Ground detection distance
```
## Best Practices
### Использование в коде
```typescript
// ✅ Good - initialization before use
this.MovementComponent.InitializeMovementSystem(
this.CharacterCapsule,
this.DebugHUDComponent
);
// ✅ Good - check initialization
if (this.MovementComponent.GetIsInitialized()) {
const surfaceType = this.MovementComponent.ClassifySurface(normal);
}
// ✅ Good - use public getters
const speed = this.MovementComponent.GetCurrentSpeed();
const state = this.MovementComponent.GetMovementState();
// ❌ Bad - direct private field access
const speed = this.MovementComponent.MaxSpeed; // Won't compile!
// ❌ Bad - use without initialization
this.MovementComponent.ClassifySurface(normal);
```
### Performance Recommendations
- Не вызывайте ProcessMovementInput() если персонаж неактивен
- Мониторьте SweepCollisionCount в debug HUD
- Используйте MaxCollisionChecks для контроля frame budget
- Кэшируйте результаты GetMaxSpeed() если используете часто
### Configuration Guidelines
- **MaxSpeed (800.0):** Оптимальная скорость для 3D платформера
- **Acceleration (10.0):** Баланс responsive feel и smoothness
- **Friction (8.0):** Чуть меньше Acceleration для natural stopping
- **Gravity (980.0):** Standard Earth gravity в UE units
- **GroundTraceDistance (5.0):** Короткая дистанция предотвращает "magnetic" effect
## Known Limitations
### Current Limitations
1. **Binary ground state** - IsGrounded true/false, нет partial contact
2. **Fixed thresholds** - Angle thresholds константны в runtime
3. **Simple sliding** - Базовый slide response, нет advanced friction models
4. **No material awareness** - Не учитывает физический материал поверхности
5. **Single capsule** - Только один collision shape
### Architectural Constraints
1. **Capsule-only collision** - Требует CapsuleComponent
2. **Frame-dependent stepping** - Sweep steps based on frame delta
3. **Limited test automation** - Collision testing требует level geometry
4. **No network optimization** - Пока не оптимизирован для multiplayer
## Планы развития
### Stage 10+: Jump System
- Добавить jump velocity application
- Jump button handling
- Coyote time для forgiveness
- Jump buffering
### Stage 11+: Steep Slope Sliding
- Sliding physics для steep slopes
- Направление slide по normal вектору
- Контроль скорости slide
### Stage 15+: Advanced Features
- Material-based friction
- Moving platform support
- Wall running mechanics
- Ledge detection
## Файловая структура
```
Content/
├── Movement/
│ ├── Components/
│ │ └── AC_Movement.ts # Core logic
│ ├── Enums/
│ │ ├── E_SurfaceType.ts # Surface types
│ │ └── E_MovementState.ts # Movement states
│ ├── Structs/
│ │ ├── S_AngleThresholds.ts # Angle thresholds
│ │ └── S_SurfaceTestCase.ts # Test case struct
│ ├── Tests/
│ │ ├── FT_MovementConfiguration.ts # ✅ Config validation
│ │ ├── FT_BasicMovement.ts # ✅ Init & state
│ │ └── FT_SurfaceClassification.ts # ✅ Surface detection
│ └── ManualTestingChecklist.md # 📝 Manual procedures
├── Math/
│ └── Libraries/
│ └── BFL_Vectors.ts # Math utilities
└── Blueprints/
└── BP_MainCharacter.ts # Integration point
```
## Troubleshooting
### Частые проблемы
**1. Character falling through ground**
- Проверить что GroundTraceDistance > 0
- Убедиться что ground имеет Visibility collision
- Проверить что CapsuleComponent инициализирован
**2. Collision checks exceeding limit**
- Уменьшить MaxSpeed
- Увеличить MaxCollisionChecks (осторожно с performance)
- Проверить что MaxStepSize не слишком маленький
**3. Jittery Z position**
- Убедиться что ground detection работает
- Проверить что ground snapping активен
- Увеличить GroundTraceDistance немного
**4. Character not rotating**
- Проверить ShouldRotateToMovement = true
- Убедиться что speed > MinSpeedForRotation
- Проверить что SetActorRotation() вызывается в EventTick
## Заключение
Movement System представляет собой production-ready детерминированную систему движения с:
**Strengths:**
- ✅ 100% coverage критических функций
- ✅ Tunneling protection через swept collision
- ✅ Deterministic physics с VInterpTo
- ✅ Comprehensive manual testing procedures
- ✅ Clear public API через getters
- ✅ Performance optimized (<1ms per frame)
**Production Status:** ✅ Ready for Stage 10
Текущее покрытие достаточно для production. Расширение TDD инфраструктуры планируется после стабилизации gameplay features.

View File

@ -1,83 +0,0 @@
// Movement/Tests/FT_BasicMovement.ts
import { AC_DebugHUD } from '#root/Debug/Components/AC_DebugHUD.ts';
import { AC_Movement } from '#root/Movement/Components/AC_Movement.ts';
import { E_MovementState } from '#root/Movement/Enums/E_MovementState.ts';
import { EFunctionalTestResult } from '#root/UE/EFunctionalTestResult.ts';
import { FunctionalTest } from '#root/UE/FunctionalTest.ts';
import { StringLibrary } from '#root/UE/StringLibrary.ts';
/**
* Functional Test: Basic Movement System
* Tests fundamental movement mechanics: acceleration, friction, max speed
* Validates movement state transitions and input processing
*/
export class FT_BasicMovement extends FunctionalTest {
// ════════════════════════════════════════════════════════════════════════════════════════
// GRAPHS
// ════════════════════════════════════════════════════════════════════════════════════════
// ────────────────────────────────────────────────────────────────────────────────────────
// EventGraph
// ────────────────────────────────────────────────────────────────────────────────────────
/**
* Test execution - validates basic movement functionality
* Tests initialization, input processing, state management
*/
EventStartTest(): void {
// Initialize movement system
this.MovementComponent.InitializeMovementSystem(
null,
this.DebugHUDComponent
);
// Test 1: Initialization
if (this.MovementComponent.GetIsInitialized()) {
// Test 2: Initial state should be Idle
if (this.MovementComponent.GetMovementState() === E_MovementState.Idle) {
// Test 3: Initial speed & velocity is zero
if (
this.MovementComponent.GetCurrentSpeed() === 0 &&
this.MovementComponent.GetCurrentVelocity().X === 0 &&
this.MovementComponent.GetCurrentVelocity().Y === 0 &&
this.MovementComponent.GetCurrentVelocity().Z === 0
) {
this.FinishTest(EFunctionalTestResult.Succeeded);
} else {
this.FinishTest(
EFunctionalTestResult.Failed,
`Current Speed & Current velocity should be zero, got Speed: ${this.MovementComponent.GetCurrentSpeed()}, Velocity: ${StringLibrary.ConvVectorToString(this.MovementComponent.GetCurrentVelocity())}`
);
}
} else {
this.FinishTest(
EFunctionalTestResult.Failed,
`Initial movement state should be Idle, got ${this.MovementComponent.GetMovementState()}`
);
}
} else {
this.FinishTest(
EFunctionalTestResult.Failed,
'Movement system failed to initialize'
);
}
}
// ════════════════════════════════════════════════════════════════════════════════════════
// VARIABLES
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Movement system component - component under test
* @category Components
*/
private MovementComponent = new AC_Movement();
/**
* Debug HUD system - displays test status and parameters
* @category Components
*/
private DebugHUDComponent = new AC_DebugHUD();
}

Binary file not shown.

View File

@ -1,114 +0,0 @@
// Movement/Tests/FT_SurfaceClassification.ts
import { BFL_Vectors } from '#root/Math/Libraries/BFL_Vectors.ts';
import { AC_Movement } from '#root/Movement/Components/AC_Movement.ts';
import { E_SurfaceType } from '#root/Movement/Enums/E_SurfaceType.ts';
import type { S_SurfaceTestCase } from '#root/Movement/Structs/S_SurfaceTestCase.ts';
import { EFunctionalTestResult } from '#root/UE/EFunctionalTestResult.ts';
import { FunctionalTest } from '#root/UE/FunctionalTest.ts';
/**
* Functional Test: Surface Classification System
* Tests angle-based surface type detection across all boundary conditions
* Validates Walkable/SteepSlope/Wall/Ceiling classification accuracy
*/
export class FT_SurfaceClassification extends FunctionalTest {
// ════════════════════════════════════════════════════════════════════════════════════════
// GRAPHS
// ════════════════════════════════════════════════════════════════════════════════════════
// ────────────────────────────────────────────────────────────────────────────────────────
// EventGraph
// ────────────────────────────────────────────────────────────────────────────────────────
/**
* Test execution - validates surface classification for all test cases
* Tests boundary conditions and edge cases for each surface type
*/
EventStartTest(): void {
this.TestCases.forEach(
({ AngleDegrees, ExpectedType, Description }, arrayIndex) => {
const surfaceType = this.MovementComponent.ClassifySurface(
BFL_Vectors.GetNormalFromAngle(AngleDegrees)
);
if (surfaceType === ExpectedType) {
this.FinishTest(EFunctionalTestResult.Succeeded);
} else {
this.FinishTest(
EFunctionalTestResult.Failed,
`Movement Component test ${arrayIndex + 1} FAIL: ${Description} (${AngleDegrees}°) expected ${ExpectedType}, got ${surfaceType}`
);
}
}
);
}
// ════════════════════════════════════════════════════════════════════════════════════════
// VARIABLES
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Movement system component - provides surface classification functionality
* @category Components
*/
private MovementComponent = new AC_Movement();
/**
* Comprehensive test cases covering all surface type boundaries
* Tests edge cases and typical angles for each classification
* @category Test Data
*/
private TestCases: S_SurfaceTestCase[] = [
{
AngleDegrees: 0.0,
ExpectedType: E_SurfaceType.Walkable,
Description: 'Flat surface',
},
{
AngleDegrees: 25.0,
ExpectedType: E_SurfaceType.Walkable,
Description: 'Gentle slope',
},
{
AngleDegrees: 49.0,
ExpectedType: E_SurfaceType.Walkable,
Description: 'Max walkable',
},
{
AngleDegrees: 51.0,
ExpectedType: E_SurfaceType.SteepSlope,
Description: 'Steep slope',
},
{
AngleDegrees: 70.0,
ExpectedType: E_SurfaceType.SteepSlope,
Description: 'Very steep',
},
{
AngleDegrees: 84.0,
ExpectedType: E_SurfaceType.SteepSlope,
Description: 'Max steep',
},
{
AngleDegrees: 90.0,
ExpectedType: E_SurfaceType.Wall,
Description: 'Vertical wall',
},
{
AngleDegrees: 94.0,
ExpectedType: E_SurfaceType.Wall,
Description: 'Max wall',
},
{
AngleDegrees: 120.0,
ExpectedType: E_SurfaceType.Ceiling,
Description: 'Overhang',
},
{
AngleDegrees: 180.0,
ExpectedType: E_SurfaceType.Ceiling,
Description: 'Ceiling',
},
];
}

Binary file not shown.

View File

@ -1,16 +1,16 @@
// Toasts/Components/AC_ToastSystem.ts
// Content/Toasts/Components/AC_ToastSystem.ts
import type { S_ToastMessage } from '#root/Toasts/Structs/S_ToastMessage.ts';
import type { WBP_Toast } from '#root/Toasts/UI/WBP_Toast.ts';
import { WBP_ToastContainer } from '#root/Toasts/UI/WBP_ToastContainer.ts';
import { ActorComponent } from '#root/UE/ActorComponent.ts';
import { CreateWidget } from '#root/UE/CteateWidget.ts';
import type { Float } from '#root/UE/Float.ts';
import type { Integer } from '#root/UE/Integer.ts';
import { SystemLibrary } from '#root/UE/SystemLibrary.ts';
import type { Text } from '#root/UE/Text.ts';
import { UEArray } from '#root/UE/UEArray.ts';
import { E_MessageType } from '#root/UI/Enums/E_MessageType.ts';
import type { S_ToastMessage } from '/Content/Toasts/Structs/S_ToastMessage.ts';
import type { WBP_Toast } from '/Content/Toasts/UI/WBP_Toast.ts';
import { WBP_ToastContainer } from '/Content/Toasts/UI/WBP_ToastContainer.ts';
import { ActorComponent } from '/Content/UE/ActorComponent.ts';
import { CreateWidget } from '/Content/UE/CteateWidget.ts';
import type { Float } from '/Content/UE/Float.ts';
import type { Integer } from '/Content/UE/Integer.ts';
import { SystemLibrary } from '/Content/UE/SystemLibrary.ts';
import type { Text } from '/Content/UE/Text.ts';
import { UEArray } from '/Content/UE/UEArray.ts';
import { E_MessageType } from '/Content/UI/Enums/E_MessageType.ts';
/**
* Toast Notification System Component

View File

@ -1,89 +0,0 @@
[//]: # (Toasts/ManualTestingChecklist.md)
# Toast System - Manual Testing Checklist
## Тестовая среда
- **Персонаж:** BP_MainCharacter с ShowDebugInfo = true
- **Требования:** ToastSystemComponent инициализирован
---
## 1. Отображение toast уведомлений
### 1.1 Базовое отображение
- [ ] **Toast появляются** в правильном месте на экране
- [ ] **Вертикальная укладка** - новые toast появляются снизу/сверху стека
- [ ] **Читаемость** - текст четко виден на игровом фоне
### 1.2 Цветовая схема по типам
- [ ] **Info toast** - голубой фон (B:226, G:144, R:74)
- [ ] **Success toast** - зеленый фон (B:92, G:184, R:92)
- [ ] **Warning toast** - оранжевый фон (B:78, G:173, R:240)
- [ ] **Error toast** - красный фон (B:79, G:83, R:217)
- [ ] **Debug toast** - серый фон (B:125, G:117, R:108)
---
## 2. Жизненный цикл toast
### 2.1 Автоматическое исчезновение
- [ ] **Default duration (3 секунды)** - toast исчезают через 3 секунды
- [ ] **Custom duration** - toast с заданной длительностью исчезают в нужное время
- [ ] **Плавное удаление** - toast исчезают без резких скачков
### 2.2 Лимит количества
- [ ] **MaxVisibleToasts = 5** - одновременно показано не больше 5 toast
- [ ] **Oldest removal** - при превышении лимита удаляются самые старые
- [ ] **FIFO поведение** - первый добавленный, первый удаленный
---
## 3. Интеграция с другими системами
### 3.1 Debug HUD интеграция
- [ ] **"Debug HUD Initialized"** - Success toast при инициализации Debug HUD
- [ ] **"Visual Debug Enabled/Disabled"** - Info toast при переключении F2
- [ ] **No conflicts** - toast не перекрывают debug HUD
### 3.2 Console logging
- [ ] **AlsoLogToConsole = true** - сообщения дублируются в консоль
- [ ] **Format:** "[MessageType] Message text" в консоли
- [ ] **All types logged** - все типы сообщений попадают в консоль
---
## 4. Edge cases
### 4.1 Различные типы сообщений
- [ ] **Empty message** - toast с пустым сообщением отображается
- [ ] **Long message** - длинные сообщения корректно отображаются
- [ ] **Multiline message** - сообщения с \n переносами работают
- [ ] **Special characters** - Unicode символы отображаются правильно
### 4.2 Rapid creation
- [ ] **Быстрое создание** множества toast работает стабильно
- [ ] **No memory leaks** при создании большого количества уведомлений
- [ ] **Performance stable** - система не влияет на FPS при активном использовании
---
## 5. Функциональные триггеры в игре
### 5.1 Debug HUD события
- [ ] **F1 toggle** не генерирует лишних toast
- [ ] **F2 toggle** показывает состояние Visual Debug
- [ ] **Debug HUD init** показывает success notification один раз при старте
### 5.2 System events
- [ ] **Startup messages** появляются при инициализации систем
- [ ] **No spam** - повторные события не создают избыточных toast
- [ ] **Proper timing** - toast появляются в нужный момент событий
---
## Критерии прохождения
- [ ] Все типы toast отображаются с правильными цветами
- [ ] Лимит в 5 уведомлений соблюдается
- [ ] Toast исчезают через заданное время
- [ ] Интеграция с Debug HUD работает корректно
- [ ] Console logging работает при включенной настройке

View File

@ -1,9 +1,9 @@
// Toasts/Structs/S_ToastMessage.ts
// Content/Toasts/Structs/S_ToastMessage.ts
import type { Float } from '#root/UE/Float.ts';
import type { Integer } from '#root/UE/Integer.ts';
import type { Text } from '#root/UE/Text.ts';
import type { E_MessageType } from '#root/UI/Enums/E_MessageType.ts';
import type { Float } from '/Content/UE/Float.ts';
import type { Integer } from '/Content/UE/Integer.ts';
import type { Text } from '/Content/UE/Text.ts';
import type { E_MessageType } from '/Content/UI/Enums/E_MessageType.ts';
export interface S_ToastMessage {
ID: Integer;

View File

@ -1,321 +0,0 @@
[//]: # (Toasts/TDD.md)
# Система Toast - Техническая Документация
## Обзор
Система уведомлений Toast для отображения временных информационных сообщений в игровом интерфейсе. Обеспечивает автоматическое управление жизненным циклом уведомлений, типизацию по важности и интеграцию с debug системами. Поддерживает до 5 одновременных уведомлений с автоматическим удалением устаревших.
## Архитектурные принципы
- **Автоматический lifecycle:** Самоуправляемое создание и удаление toast уведомлений
- **Типизированные сообщения:** Цветовая дифференциация по типу (Info, Success, Warning, Error, Debug)
- **Ограниченная емкость:** Контролируемое количество видимых уведомлений
- **Integration ready:** Тесная интеграция с Debug HUD и другими системами
- **Instance-editable config:** Настройки доступны для изменения в Blueprint editor
## Компоненты системы
### AC_ToastSystem (Core Component)
**Ответственности:**
- Управление жизненным циклом toast уведомлений
- Контроль максимального количества видимых toast
- Автоматическое удаление expired уведомлений
- Интеграция с UI контейнером для позиционирования
**Ключевые функции:**
- `InitializeToastSystem()` - Инициализация контейнера и системы
- `ShowToast()` - Создание нового уведомления с возвратом ID
- `UpdateToastSystem()` - Main loop для удаления expired toast
- `GetTestData()` - Возврат данных для тестирования
### WBP_ToastContainer (UI Container)
**Ответственности:**
- Вертикальное позиционирование toast уведомлений
- Автоматическое управление layout и spacing
- Добавление и удаление child toast widgets
- Viewport integration для корректного отображения
### WBP_Toast (Individual Widget)
**Ответственности:**
- Отображение текста уведомления
- Динамическое изменение цвета фона по типу сообщения
- Обновление содержимого в runtime
### BFL_Colors (Color Management Library)
**Ответственности:**
- Цветовая схема для разных типов сообщений
- Консистентная стилизация across всей системы
## Типы уведомлений
### Message Types (E_MessageType)
```typescript
enum E_MessageType {
Info = 'Info', // Общая информация
Success = 'Success', // Успешные операции
Warning = 'Warning', // Предупреждения
Error = 'Error', // Ошибки
Debug = 'Debug' // Debug информация
}
```
### Цветовая схема
- **Info:** Синий (#0066CC)
- **Success:** Зеленый (#00CC66)
- **Warning:** Оранжевый (#FF9900)
- **Error:** Красный (#CC0000)
- **Debug:** Фиолетовый (#9933CC)
## API Reference
### ShowToast()
```typescript
public ShowToast(
Message: Text = '',
Type: E_MessageType = E_MessageType.Info,
Duration: Float = 5
): Integer
```
**Описание:** Создает и отображает новое toast уведомление
**Возвращает:** Toast ID (положительное число) или -1 при неудаче
**Parameters:**
- `Message` - Текст уведомления
- `Type` - Тип сообщения (Info/Success/Warning/Error/Debug)
- `Duration` - Время отображения в секундах (по умолчанию 5)
**Примеры:**
```typescript
// Стандартное использование
this.ToastComponent.ShowToast("Save complete", E_MessageType.Success)
// Кастомная длительность
this.ToastComponent.ShowToast("Critical error!", E_MessageType.Error, 10)
```
### GetTestData()
```typescript
public GetTestData(): {
ToastWidgets: UEArray<WBP_Toast>;
MaxVisibleToasts: Integer;
IsEnabled: boolean;
}
```
**Описание:** Возвращает данные системы для тестирования
**Возвращает:** Объект с активными widgets и конфигурацией
**Использование в тестах:**
```typescript
const data = this.ToastComponent.GetTestData()
this.AssertEqual(data.ToastWidgets.length, 5, "Should not exceed max")
this.AssertEqual(data.MaxVisibleToasts, 5, "Default limit check")
this.AssertTrue(data.IsEnabled, "System should be enabled")
```
### InitializeToastSystem()
```typescript
public InitializeToastSystem(): void
```
**Описание:** Инициализирует систему, создает UI контейнер
**Обязательность:** Должна быть вызвана ДО любых вызовов ShowToast()
### UpdateToastSystem()
```typescript
public UpdateToastSystem(): void
```
**Описание:** Main loop функция, обрабатывает removal expired toast
**Вызов:** Должна вызываться каждый frame в Tick
## Алгоритмы работы
### Создание toast
```
ShowToast(Message, Type, Duration):
1. ShouldProcessToasts() - проверка IsInitialized && IsEnabled
2. Создание S_ToastMessage с уникальным ID
3. EnforceToastLimit() - удаление oldest если >= MaxVisibleToasts
4. ToastContainer.AddToast() - создание widget
5. Add в ActiveToasts и ToastWidgets
6. LogToConsole() если AlsoLogToConsole = true
7. Return ID или -1
```
### Удаление expired toast
```
RemoveExpiredToasts() в UpdateToastSystem():
1. Loop через ActiveToasts
2. Для каждого toast проверка: (CurrentTime - CreatedTime > Duration)
3. Если expired:
- ToastContainer.RemoveToast(widget)
- RemoveIndex() из ActiveToasts и ToastWidgets
```
### Контроль лимитов
```
EnforceToastLimit():
1. while (ActiveToasts.length >= MaxVisibleToasts)
2. Удаление oldest toast (index 0)
3. RemoveIndex(0) из обоих массивов
```
## Производительность
### Benchmarks
- **Инициализация:** <1ms
- **ShowToast:** <0.1ms на создание
- **UpdateToastSystem:** <0.05ms при 5 активных toast
- **Memory footprint:** ~50 байт на активный toast
## Система тестирования
### FT_ToastsSystemInitialization
**Проверяет базовую инициализацию:**
- Корректность default settings (IsEnabled = true, MaxVisibleToasts = 5)
- Успешность InitializeToastSystem()
### FT_ToastsDurationHandling
**Тестирует ID assignment:**
- ShowToast() возвращает валидные положительные ID
- Каждый toast получает уникальный ID
### FT_ToastsToastCreation
**Валидирует создание по всем типам:**
- Info, Success, Warning, Error, Debug
- Все типы создают валидные widgets
### FT_ToastLimit
**Проверяет контроль лимитов:**
- Создание MaxVisibleToasts + 3 уведомлений
- Проверка что отображается только MaxVisibleToasts
- Корректное удаление oldest при overflow
### FT_ToastsEdgeCases
**Тестирует граничные условия:**
- Empty message
- Long message (500 символов)
- Multiline message
## Интеграция с системами
### С Debug HUD System
```typescript
this.ToastComponent.ShowToast('Debug HUD Initialized', E_MessageType.Success)
```
### С Main Character
```typescript
// В EventBeginPlay
this.ToastSystemComponent.InitializeToastSystem()
// В Tick
this.ToastSystemComponent.UpdateToastSystem()
```
## Миграция с предыдущей версии
### Изменения в рефакторинге
1. ✅ Убрана структура `S_ToastSettings`
2. ✅ Переменные стали прямыми полями компонента с `@instanceEditable`
3. ✅ `ShowToast()` теперь имеет `Duration: Float = 5` (было 0)
4. ✅ `GetTestData()` возвращает расширенный объект
### Breaking Changes
#### 1. Доступ к настройкам
```typescript
// ❌ Старый код
if (this.ToastComponent.ToastSettings.IsEnabled) { }
// ✅ Новый код - используем GetTestData()
const data = this.ToastComponent.GetTestData()
if (data.IsEnabled) { }
```
#### 2. ShowToast Duration
```typescript
// ❌ Старый код - 0 означал default
this.ShowToast("Message", E_MessageType.Info, 0)
// ✅ Новый код - просто не передавать
this.ShowToast("Message", E_MessageType.Info)
```
## Best Practices
### Использование в коде
```typescript
// ✅ Хорошо - инициализация перед использованием
this.ToastSystemComponent.InitializeToastSystem()
this.ToastSystemComponent.ShowToast("Success!", E_MessageType.Success)
// ✅ Хорошо - кастомная длительность
this.ToastSystemComponent.ShowToast("Error!", E_MessageType.Error, 10.0)
// ❌ Плохо - использование без инициализации
this.ToastSystemComponent.ShowToast("Message") // вернет -1
```
### Рекомендации по типам
- **Info:** Общая информация
- **Success:** Подтверждение операций
- **Warning:** Предупреждения
- **Error:** Критические ошибки
- **Debug:** Техническая информация
### Рекомендации по Duration
- **1-2s:** Простые подтверждения
- **5s (default):** Большинство уведомлений
- **8-10s:** Errors, warnings, важные события
## Troubleshooting
### Toast не отображаются
- ✅ Проверить что `InitializeToastSystem()` вызван
- ✅ Проверить `IsEnabled = true` через `GetTestData()`
- ✅ Проверить что `UpdateToastSystem()` вызывается
### Toast исчезают слишком быстро
- ✅ Передавать кастомную Duration в ShowToast()
- ✅ Проверить что время в секундах
### Слишком много toast
- ✅ Настроить MaxVisibleToasts в Blueprint
- ✅ Группировать похожие уведомления
## Файловая структура
```
Content/
├── Toasts/
│ ├── Components/
│ │ └── AC_ToastSystem.ts
│ ├── Structs/
│ │ └── S_ToastMessage.ts
│ ├── UI/
│ │ ├── WBP_Toast.ts
│ │ └── WBP_ToastContainer.ts
│ └── Tests/
│ ├── FT_ToastLimit.ts
│ ├── FT_ToastsDurationHandling.ts
│ ├── FT_ToastsEdgeCases.ts
│ ├── FT_ToastsSystemInitialization.ts
│ └── FT_ToastsToastCreation.ts
├── UI/
│ ├── Enums/
│ │ └── E_MessageType.ts
│ └── Libraries/
│ └── BFL_Colors.ts
└── Blueprints/
└── BP_MainCharacter.ts
```
## Заключение
Toast System после рефакторинга представляет собой более чистую и maintainable архитектуру.
**Ключевые достижения:**
- ✅ Упрощена структура (убрана S_ToastSettings)
- ✅ Улучшен API с явным default Duration = 5s
- ✅ GetTestData() предоставляет доступ к конфигурации
- ✅ Instance-editable переменные для Blueprint
- ✅ Полная test coverage
- ✅ Production-ready performance

View File

@ -1,65 +0,0 @@
// Toasts/Tests/FT_ToastLimit.ts
import { AC_ToastSystem } from '#root/Toasts/Components/AC_ToastSystem.ts';
import { EFunctionalTestResult } from '#root/UE/EFunctionalTestResult.ts';
import { FunctionalTest } from '#root/UE/FunctionalTest.ts';
import { E_MessageType } from '#root/UI/Enums/E_MessageType.ts';
/**
* Functional Test: Toast System Capacity Management
* Validates that the toast system enforces MaxVisibleToasts limit correctly
* Tests that oldest toasts are removed when limit is exceeded
*/
export class FT_ToastLimit extends FunctionalTest {
// ════════════════════════════════════════════════════════════════════════════════════════
// GRAPHS
// ════════════════════════════════════════════════════════════════════════════════════════
// ────────────────────────────────────────────────────────────────────────────────────────
// EventGraph
// ────────────────────────────────────────────────────────────────────────────────────────
/**
* Test execution - validates toast limit enforcement
* Creates more toasts than allowed and verifies limit is enforced
*/
EventStartTest(): void {
this.ToastComponent.InitializeToastSystem();
// Create MaxVisibleToasts + 3 toasts to test overflow handling
for (
let i = 1;
i <= this.ToastComponent.ToastSettings.MaxVisibleToasts + 3;
i++
) {
this.ToastComponent.ShowToast(
`Limit test toast ${i}`,
E_MessageType.Info,
10
);
}
// Verify that only MaxVisibleToasts are actually visible
if (
this.ToastComponent.GetTestData().length ===
this.ToastComponent.ToastSettings.MaxVisibleToasts
) {
this.FinishTest(EFunctionalTestResult.Succeeded);
} else {
this.FinishTest(
EFunctionalTestResult.Failed,
`Expected ${this.ToastComponent.ToastSettings.MaxVisibleToasts} to display, got ${this.ToastComponent.GetTestData().length}`
);
}
}
// ════════════════════════════════════════════════════════════════════════════════════════
// VARIABLES
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Toast notification system - component under test
* @category Components
*/
private ToastComponent = new AC_ToastSystem();
}

BIN
Content/Toasts/Tests/FT_ToastLimit.uasset (Stored with Git LFS)

Binary file not shown.

View File

@ -1,65 +0,0 @@
// Toasts/Tests/FT_ToastsDurationHandling.ts
import { AC_ToastSystem } from '#root/Toasts/Components/AC_ToastSystem.ts';
import { EFunctionalTestResult } from '#root/UE/EFunctionalTestResult.ts';
import { FunctionalTest } from '#root/UE/FunctionalTest.ts';
import type { Integer } from '#root/UE/Integer.ts';
/**
* Functional Test: Toast Duration and Lifecycle Management
* Validates basic toast creation and ID assignment functionality
* Tests that toasts return valid IDs when created successfully
*/
export class FT_ToastsDurationHandling extends FunctionalTest {
// ════════════════════════════════════════════════════════════════════════════════════════
// GRAPHS
// ════════════════════════════════════════════════════════════════════════════════════════
// ────────────────────────────────────────────────────────────────────────────────────────
// EventGraph
// ────────────────────────────────────────────────────────────────────────────────────────
/**
* Test execution - validates basic toast creation functionality
* Creates two toasts and verifies they return valid IDs
*/
EventStartTest(): void {
this.ToastComponent.InitializeToastSystem();
this.toast1 = this.ToastComponent.ShowToast();
this.toast2 = this.ToastComponent.ShowToast();
/**
* Check if both toasts were created successfully by verifying positive IDs
*/
const AreToastsCreatedSuccessfully = (): boolean =>
this.toast1 > 0 && this.toast2 > 0;
if (AreToastsCreatedSuccessfully()) {
this.FinishTest(EFunctionalTestResult.Succeeded);
} else {
this.FinishTest(EFunctionalTestResult.Failed, `Failed to create toasts`);
}
}
// ════════════════════════════════════════════════════════════════════════════════════════
// VARIABLES
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Toast notification system - component under test
* @category Components
*/
private ToastComponent = new AC_ToastSystem();
/**
* ID of first test toast
* @category Test State
*/
private toast1: Integer = 0;
/**
* ID of second test toast
* @category Test State
*/
private toast2: Integer = 0;
}

Binary file not shown.

View File

@ -1,105 +0,0 @@
// Toasts/Tests/FT_ToastsEdgeCases.ts
import { AC_ToastSystem } from '#root/Toasts/Components/AC_ToastSystem.ts';
import { EFunctionalTestResult } from '#root/UE/EFunctionalTestResult.ts';
import { FunctionalTest } from '#root/UE/FunctionalTest.ts';
import type { Integer } from '#root/UE/Integer.ts';
import { StringLibrary } from '#root/UE/StringLibrary.ts';
import type { Text } from '#root/UE/Text.ts';
import { TextLibrary } from '#root/UE/TextLibrary.ts';
/**
* Functional Test: Toast System Edge Cases
* Tests toast system robustness with unusual input conditions
* Validates handling of empty, very long, and multiline messages
*/
export class FT_ToastsEdgeCases extends FunctionalTest {
// ════════════════════════════════════════════════════════════════════════════════════════
// GRAPHS
// ════════════════════════════════════════════════════════════════════════════════════════
// ────────────────────────────────────────────────────────────────────────────────────────
// EventGraph
// ────────────────────────────────────────────────────────────────────────────────────────
/**
* Test preparation - generates very long text for stress testing
* Creates 500-character string to test text handling limits
*/
EventPrepareTest(): void {
for (let i = 0; i < 500; i++) {
this.longText = TextLibrary.StringToText(
StringLibrary.Append(TextLibrary.TextToString(this.longText), 'A')
);
}
}
/**
* Test execution - validates edge case handling
* Tests empty text, very long text, and multiline text scenarios
*/
EventStartTest(): void {
this.ToastComponent.InitializeToastSystem();
// Test empty message handling
this.emptyToast = this.ToastComponent.ShowToast();
// Test very long message handling (500 characters)
this.longToast = this.ToastComponent.ShowToast(this.longText);
// Test multiline message handling
this.specialToast = this.ToastComponent.ShowToast(`Test
Multiline
Message`);
/**
* Check if all edge case toasts were created successfully
*/
const AreToastsCreatedSuccessfully = (): boolean =>
this.emptyToast > 0 && this.longToast > 0 && this.specialToast > 0;
if (AreToastsCreatedSuccessfully()) {
this.FinishTest(EFunctionalTestResult.Succeeded);
} else {
this.FinishTest(
EFunctionalTestResult.Failed,
`Edge case failures: empty=${this.emptyToast}, long=${this.longToast}, special=${this.specialToast}`
);
}
}
// ════════════════════════════════════════════════════════════════════════════════════════
// VARIABLES
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Toast notification system - component under test
* @category Components
*/
private ToastComponent = new AC_ToastSystem();
/**
* ID of toast created with empty message
* @category Test State
*/
private emptyToast: Integer = 0;
/**
* ID of toast created with very long message (500 chars)
* @category Test State
*/
private longToast: Integer = 0;
/**
* ID of toast created with multiline message
* @category Test State
*/
private specialToast: Integer = 0;
/**
* Generated long text string for stress testing
* Built during test preparation phase
* @category Test Data
*/
private longText: Text = '';
}

Binary file not shown.

View File

@ -1,54 +0,0 @@
// Toasts/Tests/FT_ToastsSystemInitialization.ts
import { AC_ToastSystem } from '#root/Toasts/Components/AC_ToastSystem.ts';
import { EFunctionalTestResult } from '#root/UE/EFunctionalTestResult.ts';
import { FunctionalTest } from '#root/UE/FunctionalTest.ts';
/**
* Functional Test: Toast System Initialization
* Validates that toast system initializes with correct default settings
* Tests IsEnabled state and MaxVisibleToasts configuration
*/
export class FT_ToastsSystemInitialization extends FunctionalTest {
// ════════════════════════════════════════════════════════════════════════════════════════
// GRAPHS
// ════════════════════════════════════════════════════════════════════════════════════════
// ────────────────────────────────────────────────────────────────────────────────────────
// EventGraph
// ────────────────────────────────────────────────────────────────────────────────────────
/**
* Test execution - validates initialization and default settings
* Uses nested validation to check system state after initialization
*/
EventStartTest(): void {
this.ToastComponent.InitializeToastSystem();
if (this.ToastComponent.ToastSettings.IsEnabled) {
if (this.ToastComponent.ToastSettings.MaxVisibleToasts > 0) {
this.FinishTest(EFunctionalTestResult.Succeeded);
} else {
this.FinishTest(
EFunctionalTestResult.Failed,
'Invalid state: max visible toasts less then 1'
);
}
} else {
this.FinishTest(
EFunctionalTestResult.Failed,
'Invalid state: enabled=false'
);
}
}
// ════════════════════════════════════════════════════════════════════════════════════════
// VARIABLES
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Toast notification system - component under test
* @category Components
*/
private ToastComponent = new AC_ToastSystem();
}

Binary file not shown.

View File

@ -1,71 +0,0 @@
// Toasts/Tests/FT_ToastsToastCreation.ts
import { AC_ToastSystem } from '#root/Toasts/Components/AC_ToastSystem.ts';
import { EFunctionalTestResult } from '#root/UE/EFunctionalTestResult.ts';
import { FunctionalTest } from '#root/UE/FunctionalTest.ts';
import { UEArray } from '#root/UE/UEArray.ts';
import { E_MessageType } from '#root/UI/Enums/E_MessageType.ts';
/**
* Functional Test: Toast Creation by Message Type
* Validates toast creation functionality for all message types
* Tests that each type (Info, Success, Warning, Error, Debug) creates valid toasts
*/
export class FT_ToastsToastCreation extends FunctionalTest {
// ════════════════════════════════════════════════════════════════════════════════════════
// GRAPHS
// ════════════════════════════════════════════════════════════════════════════════════════
// ────────────────────────────────────────────────────────────────────────────────────────
// EventGraph
// ────────────────────────────────────────────────────────────────────────────────────────
/**
* Test execution - validates toast creation for each message type
* Iterates through all message types and verifies successful creation
*/
EventStartTest(): void {
this.ToastComponent.InitializeToastSystem();
this.ToastTypes.forEach(arrayElement => {
// Create toast for current message type and check if valid ID is returned
if (
this.ToastComponent.ShowToast(
`Test ${arrayElement} message`,
arrayElement,
1
) <= 0
) {
this.FinishTest(
EFunctionalTestResult.Failed,
`Failed to create ${arrayElement} toast`
);
}
});
this.FinishTest(EFunctionalTestResult.Succeeded);
}
// ════════════════════════════════════════════════════════════════════════════════════════
// VARIABLES
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Toast notification system - component under test
* @category Components
*/
private ToastComponent = new AC_ToastSystem();
/**
* Array of all message types to test
* Covers complete range of supported toast types
* @category Test Data
*/
private ToastTypes: UEArray<E_MessageType> = new UEArray([
E_MessageType.Info,
E_MessageType.Success,
E_MessageType.Warning,
E_MessageType.Error,
E_MessageType.Debug,
]);
}

Binary file not shown.

View File

@ -1,13 +1,13 @@
// Toasts/UI/WBP_Toast.ts
// Content/Toasts/UI/WBP_Toast.ts
import { Border } from '#root/UE/Border.ts';
import { MathLibrary } from '#root/UE/MathLibrary.ts';
import { SystemLibrary } from '#root/UE/SystemLibrary.ts';
import type { Text } from '#root/UE/Text.ts';
import { TextBlock } from '#root/UE/TextBlock.ts';
import { UserWidget } from '#root/UE/UserWidget.ts';
import { E_MessageType } from '#root/UI/Enums/E_MessageType.ts';
import { BFL_Colors } from '#root/UI/Libraries/BFL_Colors.ts';
import { Border } from '/Content/UE/Border.ts';
import { MathLibrary } from '/Content/UE/MathLibrary.ts';
import { SystemLibrary } from '/Content/UE/SystemLibrary.ts';
import type { Text } from '/Content/UE/Text.ts';
import { TextBlock } from '/Content/UE/TextBlock.ts';
import { UserWidget } from '/Content/UE/UserWidget.ts';
import { E_MessageType } from '/Content/UI/Enums/E_MessageType.ts';
import { BFL_Colors } from '/Content/UI/Libraries/BFL_Colors.ts';
/**
* Individual Toast Notification Widget

View File

@ -1,14 +1,14 @@
// Toasts/UI/WBP_ToastContainer.ts
// Content/Toasts/UI/WBP_ToastContainer.ts
import { WBP_Toast } from '#root/Toasts/UI/WBP_Toast.ts';
import { CreateWidget } from '#root/UE/CteateWidget.ts';
import { ESlateVisibility } from '#root/UE/ESlateVisibility.ts';
import { SystemLibrary } from '#root/UE/SystemLibrary.ts';
import type { Text } from '#root/UE/Text.ts';
import { UEArray } from '#root/UE/UEArray.ts';
import { UserWidget } from '#root/UE/UserWidget.ts';
import { VerticalBox } from '#root/UE/VerticalBox.ts';
import type { E_MessageType } from '#root/UI/Enums/E_MessageType.ts';
import { WBP_Toast } from '/Content/Toasts/UI/WBP_Toast.ts';
import { CreateWidget } from '/Content/UE/CteateWidget.ts';
import { ESlateVisibility } from '/Content/UE/ESlateVisibility.ts';
import { SystemLibrary } from '/Content/UE/SystemLibrary.ts';
import type { Text } from '/Content/UE/Text.ts';
import { UEArray } from '/Content/UE/UEArray.ts';
import { UserWidget } from '/Content/UE/UserWidget.ts';
import { VerticalBox } from '/Content/UE/VerticalBox.ts';
import type { E_MessageType } from '/Content/UI/Enums/E_MessageType.ts';
/**
* Toast Container Widget

View File

@ -1,15 +1,19 @@
// UE/Actor.ts
// Content/UE/Actor.ts
import { Name } from '#root/UE/Name.ts';
import { Rotator } from '#root/UE/Rotator.ts';
import { UEObject } from '#root/UE/UEObject.ts';
import { Vector } from '#root/UE/Vector.ts';
import { Name } from '/Content/UE/Name.ts';
import { Rotator } from '/Content/UE/Rotator.ts';
import { UEObject } from '/Content/UE/UEObject.ts';
import { Vector } from '/Content/UE/Vector.ts';
export class Actor extends UEObject {
constructor(outer: UEObject | null = null, name: Name | string = Name.NONE) {
super(outer, name);
}
public GetActorLocation(): Vector {
return new Vector(); // Placeholder implementation
}
public SetActorLocation(
NewLocation: Vector = new Vector(),
Sweep: boolean = false,
@ -19,6 +23,10 @@ export class Actor extends UEObject {
// Implementation for setting actor location
}
public GetActorRotation(): Rotator {
return new Rotator(); // Placeholder implementation
}
public SetActorRotation(
NewRotation: Rotator = new Rotator(),
TeleportPhysics: boolean = false
@ -26,8 +34,4 @@ export class Actor extends UEObject {
console.log(NewRotation, TeleportPhysics);
// Implementation for setting actor rotation
}
public GetActorLocation(): Vector {
return new Vector(); // Placeholder implementation
}
}

View File

@ -1,8 +1,8 @@
// UE/ActorComponent.ts
// Content/UE/ActorComponent.ts
import { Actor } from '#root/UE/Actor.ts';
import { Name } from '#root/UE/Name.ts';
import { UEObject } from '#root/UE/UEObject.ts';
import { Actor } from '/Content/UE/Actor.ts';
import { Name } from '/Content/UE/Name.ts';
import { UEObject } from '/Content/UE/UEObject.ts';
export class ActorComponent extends UEObject {
constructor(outer: UEObject | null = null, name: Name | string = Name.NONE) {

View File

@ -1,3 +1,3 @@
// UE/BitmaskInteger.ts
// Content/UE/BitmaskInteger.ts
export type BitmaskInteger = number;

View File

@ -1,6 +1,6 @@
// UE/BlueprintFunctionLibrary.ts
// Content/UE/BlueprintFunctionLibrary.ts
import { UEObject } from '#root/UE/UEObject.ts';
import { UEObject } from '/Content/UE/UEObject.ts';
export class BlueprintFunctionLibrary extends UEObject {
constructor(

View File

@ -1,9 +1,9 @@
// UE/Border.ts
// Content/UE/Border.ts
import { ContentWidget } from '#root/UE/ContentWidget.ts';
import type { LinearColor } from '#root/UE/LinearColor.ts';
import { Name } from '#root/UE/Name.ts';
import { UEObject } from '#root/UE/UEObject.ts';
import { ContentWidget } from '/Content/UE/ContentWidget.ts';
import type { LinearColor } from '/Content/UE/LinearColor.ts';
import { Name } from '/Content/UE/Name.ts';
import { UEObject } from '/Content/UE/UEObject.ts';
export class Border extends ContentWidget {
constructor(outer: UEObject | null = null, name: Name | string = Name.NONE) {

View File

@ -1,3 +1,3 @@
// UE/Byte.ts
// Content/UE/Byte.ts
export type Byte = number;

View File

@ -1,5 +1,7 @@
import type { Float } from '#root/UE/Float.ts';
import { ShapeComponent } from '#root/UE/ShapeComponent.ts';
// Content/UE/CapsuleComponen.ts
import type { Float } from '/Content/UE/Float.ts';
import { ShapeComponent } from '/Content/UE/ShapeComponent.ts';
export class CapsuleComponent extends ShapeComponent {
constructor(outer: ShapeComponent | null = null, name: string = 'None') {

View File

@ -1,4 +1,4 @@
// Content/Cast.ts
// Content/UE/Cast.ts
export function Cast<T>(obj: unknown): T | null {
return (obj as T) || null;

View File

@ -1,7 +1,7 @@
// UE/Color.ts
// Content/UE/Color.ts
import { StructBase } from '#root/UE/StructBase.ts';
import type { UInt8 } from '#root/UE/UInt8.ts';
import { StructBase } from '/Content/UE/StructBase.ts';
import type { UInt8 } from '/Content/UE/UInt8.ts';
export class Color extends StructBase {
constructor(

View File

@ -1,8 +1,8 @@
// UE/ContentWidget.ts
// Content/UE/ContentWidget.ts
import { Name } from '#root/UE/Name.ts';
import { PanelWidget } from '#root/UE/PanelWidget.ts';
import { UEObject } from '#root/UE/UEObject.ts';
import { Name } from '/Content/UE/Name.ts';
import { PanelWidget } from '/Content/UE/PanelWidget.ts';
import { UEObject } from '/Content/UE/UEObject.ts';
export class ContentWidget extends PanelWidget {
constructor(outer: UEObject | null = null, name: Name | string = Name.NONE) {

View File

@ -1,9 +1,9 @@
// UE/Controller.ts
// Content/UE/Controller.ts
import { Actor } from '#root/UE/Actor.ts';
import { Name } from '#root/UE/Name.ts';
import type { Rotator } from '#root/UE/Rotator.ts';
import { UEObject } from '#root/UE/UEObject.ts';
import { Actor } from '/Content/UE/Actor.ts';
import { Name } from '/Content/UE/Name.ts';
import type { Rotator } from '/Content/UE/Rotator.ts';
import { UEObject } from '/Content/UE/UEObject.ts';
export class Controller extends Actor {
constructor(outer: UEObject | null = null, name: Name | string = Name.NONE) {

View File

@ -1,6 +1,6 @@
// UE/CreateWidget.ts
// Content/UE/CreateWidget.ts
import type { UserWidget } from '#root/UE/UserWidget.ts';
import type { UserWidget } from '/Content/UE/UserWidget.ts';
type WidgetConstructor<T extends UserWidget> = new () => T;

View File

@ -1,7 +1,7 @@
// UE/DataAsset.ts
// Content/UE/DataAsset.ts
import { Name } from '#root/UE/Name.ts';
import { UEObject } from '#root/UE/UEObject.ts';
import { Name } from '/Content/UE/Name.ts';
import { UEObject } from '/Content/UE/UEObject.ts';
export class DataAsset extends UEObject {
constructor(outer: UEObject | null = null, name: Name | string = Name.NONE) {

Some files were not shown because too many files have changed in this diff Show More