rewrite movement to c++

main
Nikolay Petrov 2025-12-24 01:09:43 +05:00
parent 9539f48a06
commit 963e7a34dc
196 changed files with 1632 additions and 9565 deletions

View File

@ -43,13 +43,6 @@ module.exports = {
'sibling', 'sibling',
'index', 'index',
], ],
'pathGroups': [
{
'pattern': '#root/**',
'group': 'internal',
'position': 'before'
}
],
'pathGroupsExcludedImportTypes': ['builtin'], 'pathGroupsExcludedImportTypes': ['builtin'],
'newlines-between': 'never', 'newlines-between': 'never',
'alphabetize': { 'alphabetize': {

View File

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

View File

@ -5,3 +5,5 @@ CommonButtonAcceptKeyHandling=TriggerClick
[/Script/EngineSettings.GeneralProjectSettings] [/Script/EngineSettings.GeneralProjectSettings]
ProjectID=56CEA3524FAE49EC0DF6D8A5178FEC04 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_Camera } from '/Content/Camera/AC_Camera.ts';
import { AC_DebugHUD } from '#root/Debug/Components/AC_DebugHUD.ts'; import { AC_DebugHUD } from '/Content/Debug/Components/AC_DebugHUD.ts';
import { AC_InputDevice } from '#root/Input/Components/AC_InputDevice.ts'; import { AC_InputDevice } from '/Content/Input/Components/AC_InputDevice.ts';
import { IMC_Default } from '#root/Input/IMC_Default.ts'; import { IMC_Default } from '/Content/Input/IMC_Default.ts';
import { AC_Movement } from '#root/Movement/AC_Movement.ts'; import { AC_ToastSystem } from '/Content/Toasts/Components/AC_ToastSystem.ts';
import { AC_ToastSystem } from '#root/Toasts/Components/AC_ToastSystem.ts'; import { CapsuleComponent } from '/Content/UE/CapsuleComponent.ts';
import { CapsuleComponent } from '#root/UE/CapsuleComponent.ts'; import { Cast } from '/Content/UE/Cast.ts';
import { Cast } from '#root/UE/Cast.ts'; import type { Controller } from '/Content/UE/Controller.ts';
import type { Controller } from '#root/UE/Controller.ts'; import { EnhancedInputLocalPlayerSubsystem } from '/Content/UE/EnhancedInputLocalPlayerSubsystem.ts';
import { EnhancedInputLocalPlayerSubsystem } from '#root/UE/EnhancedInputLocalPlayerSubsystem.ts'; import type { Float } from '/Content/UE/Float.ts';
import type { Float } from '#root/UE/Float.ts'; import { MathLibrary } from '/Content/UE/MathLibrary.ts';
import { MathLibrary } from '#root/UE/MathLibrary.ts'; import { Pawn } from '/Content/UE/Pawn.ts';
import { Pawn } from '#root/UE/Pawn.ts'; import type { PlayerController } from '/Content/UE/PlayerController.ts';
import type { PlayerController } from '#root/UE/PlayerController.ts'; import { Rotator } from '/Content/UE/Rotator.ts';
import { Rotator } from '#root/UE/Rotator.ts'; import { SystemLibrary } from '/Content/UE/SystemLibrary.ts';
import { SystemLibrary } from '#root/UE/SystemLibrary.ts'; import { Vector } from '/Content/UE/Vector.ts';
import { Vector } from '#root/UE/Vector.ts'; import { TengriMovementComponent } from '/Source/TengriPlatformer/Movement/TengriMovementComponent.ts';
import { DA_TengriMovementConfig } from '/Content/Movement/DA_TengriMovementConfig.ts';
/** /**
* Main Character Blueprint * 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); return new Vector(vec1.X + vec2.X, vec1.Y + vec2.Y, 0);
}; };
this.CurrentMovementInput = CalculateResultMovementInputVector( this.TengriMovement.SetInputVector(
MathLibrary.GetRightVector( CalculateResultMovementInputVector(
this.GetControlRotation().roll, MathLibrary.GetRightVector(
0, this.GetControlRotation().roll,
this.GetControlRotation().yaw 0,
), this.GetControlRotation().yaw
MathLibrary.GetForwardVector(0, 0, this.GetControlRotation().yaw), ),
ActionValueX, MathLibrary.GetForwardVector(0, 0, this.GetControlRotation().yaw),
ActionValueY ActionValueX,
ActionValueY
)
); );
} }
@ -139,7 +142,7 @@ export class BP_MainCharacter extends Pawn {
* Reset movement input when move action is completed * Reset movement input when move action is completed
*/ */
EnhancedInputActionMoveCompleted(): void { 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.CameraComponent.InitializeCameraSystem(
this.InputDeviceComponent, this.InputDeviceComponent,
this.DebugHUDComponent this.DebugHUDComponent
@ -196,13 +194,7 @@ export class BP_MainCharacter extends Pawn {
) )
); );
this.MovementComponent.ProcessMovementInput(
this.CurrentMovementInput,
DeltaTime
);
if (this.ShowDebugInfo) { if (this.ShowDebugInfo) {
this.MovementComponent.UpdateDebugPage();
this.InputDeviceComponent.UpdateDebugPage(); this.InputDeviceComponent.UpdateDebugPage();
this.CameraComponent.UpdateDebugPage(); this.CameraComponent.UpdateDebugPage();
} }
@ -230,6 +222,8 @@ export class BP_MainCharacter extends Pawn {
*/ */
ToastSystemComponent = new AC_ToastSystem(); ToastSystemComponent = new AC_ToastSystem();
TengriMovement = new TengriMovementComponent(DA_TengriMovementConfig);
/** /**
* Debug HUD system - displays movement parameters and performance metrics * Debug HUD system - displays movement parameters and performance metrics
* @category Components * @category Components
@ -242,12 +236,6 @@ export class BP_MainCharacter extends Pawn {
*/ */
CharacterCapsule = new CapsuleComponent(); 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) * Master debug toggle - controls all debug systems (HUD, toasts, visual debug)
* @category Debug * @category Debug
@ -259,9 +247,4 @@ export class BP_MainCharacter extends Pawn {
* Cached delta time from last tick - used for time-based calculations * Cached delta time from last tick - used for time-based calculations
*/ */
private DeltaTime: Float = 0.0; 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 // Blueprints/BP_TengriGameMode.ts
import { BP_MainCharacter } from '#root/Blueprints/BP_MainCharacter.ts'; import { BP_MainCharacter } from '/Content/Blueprints/BP_MainCharacter.ts';
import { GameModeBase } from '#root/UE/GameModeBase.ts'; import { GameModeBase } from '/Content/UE/GameModeBase.ts';
export class BP_TengriGameMode extends GameModeBase { export class BP_TengriGameMode extends GameModeBase {
DefaultPawnClass = BP_MainCharacter; 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_DebugHUD } from '/Content/Debug/Components/AC_DebugHUD.ts';
import type { AC_InputDevice } from '#root/Input/Components/AC_InputDevice.ts'; import type { AC_InputDevice } from '/Content/Input/Components/AC_InputDevice.ts';
import { ActorComponent } from '#root/UE/ActorComponent.ts'; import { ActorComponent } from '/Content/UE/ActorComponent.ts';
import type { Float } from '#root/UE/Float.ts'; import type { Float } from '/Content/UE/Float.ts';
import { MathLibrary } from '#root/UE/MathLibrary.ts'; import { MathLibrary } from '/Content/UE/MathLibrary.ts';
import { SystemLibrary } from '#root/UE/SystemLibrary.ts'; import { SystemLibrary } from '/Content/UE/SystemLibrary.ts';
import { Vector } from '#root/UE/Vector.ts'; import { Vector } from '/Content/UE/Vector.ts';
/** /**
* Camera System Component * 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 type { S_DebugPage } from '/Content/Debug/Structs/S_DebugPage.ts';
import { WBP_DebugHUD } from '#root/Debug/UI/WBP_DebugHUD.ts'; import { WBP_DebugHUD } from '/Content/Debug/UI/WBP_DebugHUD.ts';
import type { AC_InputDevice } from '#root/Input/Components/AC_InputDevice.ts'; import type { AC_InputDevice } from '/Content/Input/Components/AC_InputDevice.ts';
import type { AC_ToastSystem } from '#root/Toasts/Components/AC_ToastSystem.ts'; import type { AC_ToastSystem } from '/Content/Toasts/Components/AC_ToastSystem.ts';
import { ActorComponent } from '#root/UE/ActorComponent.ts'; import { ActorComponent } from '/Content/UE/ActorComponent.ts';
import { CreateWidget } from '#root/UE/CteateWidget.ts'; import { CreateWidget } from '/Content/UE/CteateWidget.ts';
import { ESlateVisibility } from '#root/UE/ESlateVisibility.ts'; import { ESlateVisibility } from '/Content/UE/ESlateVisibility.ts';
import type { Float } from '#root/UE/Float.ts'; import type { Float } from '/Content/UE/Float.ts';
import type { Integer } from '#root/UE/Integer.ts'; import type { Integer } from '/Content/UE/Integer.ts';
import { SystemLibrary } from '#root/UE/SystemLibrary.ts'; import { SystemLibrary } from '/Content/UE/SystemLibrary.ts';
import type { Text } from '#root/UE/Text.ts'; import type { Text } from '/Content/UE/Text.ts';
import { UEArray } from '#root/UE/UEArray.ts'; import { UEArray } from '/Content/UE/UEArray.ts';
import { E_MessageType } from '#root/UI/Enums/E_MessageType.ts'; import { E_MessageType } from '/Content/UI/Enums/E_MessageType.ts';
/** /**
* Debug HUD Controller Component * Debug HUD Controller Component

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 { Float } from '/Content/UE/Float.ts';
import type { Text } from '#root/UE/Text.ts'; import type { Text } from '/Content/UE/Text.ts';
export interface S_DebugPage { export interface S_DebugPage {
PageID: string; 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/AC_Movement.ts'; import { SystemLibrary } from '/Content/UE/SystemLibrary.ts';
import { SystemLibrary } from '#root/UE/SystemLibrary.ts'; import type { Text } from '/Content/UE/Text.ts';
import type { Text } from '#root/UE/Text.ts'; import { TextBlock } from '/Content/UE/TextBlock.ts';
import { TextBlock } from '#root/UE/TextBlock.ts'; import { UserWidget } from '/Content/UE/UserWidget.ts';
import { UserWidget } from '#root/UE/UserWidget.ts';
/** /**
* Debug HUD Widget for displaying system information * Debug HUD Widget for displaying system information
@ -107,13 +106,6 @@ export class WBP_DebugHUD extends UserWidget {
// VARIABLES // 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 * Current page title text
* Updated by AC_DebugHUD when switching pages * 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 { InputAction } from '/Content/UE/InputAction.ts';
import { Name } from '#root/UE/Name.ts'; import { Name } from '/Content/UE/Name.ts';
export const IA_LeftTrigger = new InputAction(null, new Name('IA_LeftTrigger')); 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 { InputAction } from '/Content/UE/InputAction.ts';
import { Name } from '#root/UE/Name.ts'; import { Name } from '/Content/UE/Name.ts';
export const IA_Look = new InputAction(null, new Name('IA_Look')); 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 { InputAction } from '/Content/UE/InputAction.ts';
import { Name } from '#root/UE/Name.ts'; import { Name } from '/Content/UE/Name.ts';
export const IA_Move = new InputAction(null, new Name('IA_Move')); 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'); 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'); 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'); 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'); 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( export const IA_ToggleVisualDebug = new InputAction(
null, 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_DebugHUD } from '/Content/Debug/Components/AC_DebugHUD.ts';
import type { AC_ToastSystem } from '#root/Toasts/Components/AC_ToastSystem.ts'; import type { AC_ToastSystem } from '/Content/Toasts/Components/AC_ToastSystem.ts';
import { ActorComponent } from '#root/UE/ActorComponent.ts'; import { ActorComponent } from '/Content/UE/ActorComponent.ts';
import { EHardwareDevicePrimaryType } from '#root/UE/EHardwareDevicePrimaryType.ts'; import { EHardwareDevicePrimaryType } from '/Content/UE/EHardwareDevicePrimaryType.ts';
import type { Float } from '#root/UE/Float.ts'; import type { Float } from '/Content/UE/Float.ts';
import { InputDeviceSubsystem } from '#root/UE/InputDeviceSubsystem.ts'; import { InputDeviceSubsystem } from '/Content/UE/InputDeviceSubsystem.ts';
import type { Integer } from '#root/UE/Integer.ts'; import type { Integer } from '/Content/UE/Integer.ts';
import { SystemLibrary } from '#root/UE/SystemLibrary.ts'; import { SystemLibrary } from '/Content/UE/SystemLibrary.ts';
import { E_MessageType } from '#root/UI/Enums/E_MessageType.ts'; import { E_MessageType } from '/Content/UI/Enums/E_MessageType.ts';
/** /**
* Input Device Detection Component * 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_LeftTrigger } from '/Content/Input/Actions/IA_LeftTrigger.ts';
import { IA_Look } from '#root/Input/Actions/IA_Look.ts'; import { IA_Look } from '/Content/Input/Actions/IA_Look.ts';
import { IA_Move } from '#root/Input/Actions/IA_Move.ts'; import { IA_Move } from '/Content/Input/Actions/IA_Move.ts';
import { IA_NextDebugMode } from '#root/Input/Actions/IA_NextDebugMode.ts'; import { IA_NextDebugMode } from '/Content/Input/Actions/IA_NextDebugMode.ts';
import { IA_PrevDebugMode } from '#root/Input/Actions/IA_PrevDebugMode.ts'; import { IA_PrevDebugMode } from '/Content/Input/Actions/IA_PrevDebugMode.ts';
import { IA_RightTrigger } from '#root/Input/Actions/IA_RightTrigger.ts'; import { IA_RightTrigger } from '/Content/Input/Actions/IA_RightTrigger.ts';
import { IA_ToggleHUD } from '#root/Input/Actions/IA_ToggleHUD.ts'; import { IA_ToggleHUD } from '/Content/Input/Actions/IA_ToggleHUD.ts';
import { IA_ToggleVisualDebug } from '#root/Input/Actions/IA_ToggleVisualDebug.ts'; import { IA_ToggleVisualDebug } from '/Content/Input/Actions/IA_ToggleVisualDebug.ts';
import { InputMappingContext } from '#root/UE/InputMappingContext.ts'; import { InputMappingContext } from '/Content/UE/InputMappingContext.ts';
import { Key } from '#root/UE/Key.ts'; import { Key } from '/Content/UE/Key.ts';
export const IMC_Default = new InputMappingContext(); 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,59 +1,5 @@
// Levels/TestLevel.ts // Content/Levels/TestLevel.ts
import { BP_MainCharacter } from '#root/Blueprints/BP_MainCharacter.ts'; import { BP_MainCharacter } from '/Content/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_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';
new BP_MainCharacter(); 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();
// 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.

View File

@ -1,231 +0,0 @@
// Movement/AC_Movement.ts
import type { AC_DebugHUD } from '#root/Debug/Components/AC_DebugHUD.ts';
import { BFL_MovementProcessor } from '#root/Movement/Core/BFL_MovementProcessor.ts';
import type { DA_MovementConfig } from '#root/Movement/Core/DA_MovementConfig.ts';
import { DA_MovementConfigDefault } from '#root/Movement/Core/DA_MovementConfigDefault.ts';
import { E_MovementState } from '#root/Movement/Core/E_MovementState.ts';
import type { S_MovementState } from '#root/Movement/Core/S_MovementState.ts';
import { E_SurfaceType } from '#root/Movement/Surface/E_SurfaceType.ts';
import { S_AngleThresholds } from '#root/Movement/Surface/S_AngleThresholds.ts';
import { ActorComponent } from '#root/UE/ActorComponent.ts';
import type { CapsuleComponent } from '#root/UE/CapsuleComponent.ts';
import type { Float } from '#root/UE/Float.ts';
import { HitResult } from '#root/UE/HitResult.ts';
import { MathLibrary } from '#root/UE/MathLibrary.ts';
import { Rotator } from '#root/UE/Rotator.ts';
import { StringLibrary } from '#root/UE/StringLibrary.ts';
import { SystemLibrary } from '#root/UE/SystemLibrary.ts';
import { Vector } from '#root/UE/Vector.ts';
/**
* Movement System Component
* Core deterministic movement system for 3D platformer
* Handles surface classification and movement physics calculations
*/
export class AC_Movement extends ActorComponent {
// ════════════════════════════════════════════════════════════════════════════════════════
// FUNCTIONS
// ════════════════════════════════════════════════════════════════════════════════════════
// ────────────────────────────────────────────────────────────────────────────────────────
// Debug
// ────────────────────────────────────────────────────────────────────────────────────────
/**
* Update debug HUD with current movement info
* @category Debug
*/
public UpdateDebugPage(): void {
if (SystemLibrary.IsValid(this.DebugHUDComponent)) {
if (
this.DebugHUDComponent.ShouldUpdatePage(
this.DebugPageID,
SystemLibrary.GetGameTimeInSeconds()
)
) {
this.DebugHUDComponent.UpdatePageContent(
this.DebugPageID,
// Constants
`Max Speed: ${this.Config.MaxSpeed}\n` +
`Acceleration: ${this.Config.Acceleration}\n` +
`Friction: ${this.Config.Friction}\n` +
`Gravity: ${this.Config.Gravity}\n` +
`Initialized: ${this.IsInitialized}\n` +
`\n` +
// Current State
`Current Velocity: ${StringLibrary.ConvVectorToString(this.CurrentMovementState.Velocity)}\n` +
`Speed: ${this.CurrentMovementState.Speed}\n` +
`Is Grounded: ${this.CurrentMovementState.IsGrounded}\n` +
`Surface Type: ${this.CurrentMovementState.SurfaceType}\n` +
`Movement State: ${this.CurrentMovementState.MovementState}\n` +
`Input Magnitude: ${this.CurrentMovementState.InputMagnitude}` +
`\n` +
// Rotation
`Current Yaw: ${this.CurrentMovementState.Rotation.yaw}\n` +
`Rotation Delta: ${this.CurrentMovementState.RotationDelta}\` +
`Is Rotating: ${this.CurrentMovementState.IsRotating}\n` +
`Rotation Speed: ${this.Config.RotationSpeed}\` +
`Min Speed: ${this.Config.MinSpeedForRotation}` +
`\n` +
// Position
`Location: ${StringLibrary.ConvVectorToString(this.GetOwner().GetActorLocation())}` +
`\n` +
// Sweep Collision
`Collision Checks: ${this.CurrentMovementState.CollisionCount}/${this.Config.MaxCollisionChecks}\n` +
`Sweep Blocked: ${this.CurrentMovementState.IsBlocked}\n` +
`Ground Distance: ${this.Config.GroundTraceDistance} cm`
);
}
}
}
// ────────────────────────────────────────────────────────────────────────────────────────
// Default
// ────────────────────────────────────────────────────────────────────────────────────────
/**
* Process movement input from player controller
* Normalizes input and calculates target velocity
* @param InputVector - Raw input from WASD/gamepad stick
* @param DeltaTime - Time since last frame for frame-rate independence
* @category Default
*/
public ProcessMovementInput(InputVector: Vector, DeltaTime: Float): void {
if (this.IsInitialized) {
this.CurrentMovementState = BFL_MovementProcessor.ProcessMovement(
this.CurrentMovementState,
{
InputVector,
DeltaTime,
CapsuleComponent: this.CapsuleComponent,
Config: this.Config,
AngleThresholdsRads: this.AngleThresholdsRads,
},
SystemLibrary.IsValid(this.DebugHUDComponent)
? this.DebugHUDComponent.ShowVisualDebug
: false
);
this.GetOwner().SetActorLocation(this.CurrentMovementState.Location);
this.GetOwner().SetActorRotation(this.CurrentMovementState.Rotation);
}
}
/**
* Initialize movement system with angle conversion
* Converts degree thresholds to radians for runtime performance
* @category Default
*/
public InitializeMovementSystem(
CapsuleComponentRef: CapsuleComponent | null = null,
DebugHUDComponentRef: AC_DebugHUD | null = null
): void {
this.CapsuleComponent = CapsuleComponentRef;
this.DebugHUDComponent = DebugHUDComponentRef;
this.IsInitialized = true;
this.AngleThresholdsRads = {
Walkable: MathLibrary.DegreesToRadians(
this.Config.AngleThresholdsDegrees.Walkable
),
SteepSlope: MathLibrary.DegreesToRadians(
this.Config.AngleThresholdsDegrees.SteepSlope
),
Wall: MathLibrary.DegreesToRadians(
this.Config.AngleThresholdsDegrees.Wall
),
};
this.CurrentMovementState = BFL_MovementProcessor.CreateInitialState(
this.GetOwner().GetActorLocation(),
this.GetOwner().GetActorRotation()
);
if (SystemLibrary.IsValid(this.DebugHUDComponent)) {
this.DebugHUDComponent.AddDebugPage(
this.DebugPageID,
'Movement Info',
60
);
}
}
// ════════════════════════════════════════════════════════════════════════════════════════
// VARIABLES
// ════════════════════════════════════════════════════════════════════════════════════════
// ────────────────────────────────────────────────────────────────────────────────────────
// Components
// ────────────────────────────────────────────────────────────────────────────────────────
/**
* Reference to debug HUD component for displaying camera info
* Optional, used for debugging purposes
* @category Components
*/
private DebugHUDComponent: AC_DebugHUD | null = null;
/**
* Reference to character's capsule component for collision detection
* @category Components
*/
private CapsuleComponent: CapsuleComponent | null = null;
// ────────────────────────────────────────────────────────────────────────────────────────
// Default
// ────────────────────────────────────────────────────────────────────────────────────────
/**
* Default movement state
* @category Default
*/
private CurrentMovementState: S_MovementState = {
Location: new Vector(0, 0, 0),
Rotation: new Rotator(0, 0, 0),
Velocity: new Vector(0, 0, 0),
Speed: 0.0,
IsGrounded: false,
GroundHit: new HitResult(),
SurfaceType: E_SurfaceType.None,
IsBlocked: false,
CollisionCount: 0,
IsRotating: false,
RotationDelta: 0.0,
MovementState: E_MovementState.Idle,
InputMagnitude: 0.0,
};
/**
* Default movement configuration
* @category Default
* @instanceEditable true
*/
private readonly Config: DA_MovementConfig = new DA_MovementConfigDefault();
/**
* Runtime cached angle thresholds in radians
* Converted from degrees during initialization for performance
* @category Default
*/
private AngleThresholdsRads: S_AngleThresholds = {
Walkable: 0.0,
SteepSlope: 0.0,
Wall: 0.0,
};
/**
* Debug page identifier for organizing debug output
* Used by debug HUD to categorize information
* @category Default
* @instanceEditable true
*/
private readonly DebugPageID: string = 'MovementInfo';
/**
* Flag indicating if movement system has been initialized
* Ensures angle thresholds are converted before use
* @category Debug
*/
private IsInitialized = false;
}

BIN
Content/Movement/AC_Movement.uasset (Stored with Git LFS)

Binary file not shown.

View File

@ -1,325 +0,0 @@
// Movement/Collision/BFL_CollisionResolver.ts
import type { S_SweepResult } from '#root/Movement/Collision/S_SweepResult.ts';
import { DA_MovementConfig } from '#root/Movement/Core/DA_MovementConfig.ts';
import { BlueprintFunctionLibrary } from '#root/UE/BlueprintFunctionLibrary.ts';
import type { CapsuleComponent } from '#root/UE/CapsuleComponent.ts';
import { EDrawDebugTrace } from '#root/UE/EDrawDebugTrace.ts';
import { ETraceTypeQuery } from '#root/UE/ETraceTypeQuery.ts';
import type { Float } from '#root/UE/Float.ts';
import { HitResult } from '#root/UE/HitResult.ts';
import type { Integer } from '#root/UE/Integer.ts';
import { MathLibrary } from '#root/UE/MathLibrary.ts';
import { SystemLibrary } from '#root/UE/SystemLibrary.ts';
import { Vector } from '#root/UE/Vector.ts';
/**
* Collision Resolution System
*
* Handles swept collision detection and surface sliding
* Prevents tunneling through geometry with adaptive stepping
* Provides deterministic collision response
*
* @category Movement Collision
* @impure Uses SystemLibrary traces (reads world state)
*/
class BFL_CollisionResolverClass extends BlueprintFunctionLibrary {
// ════════════════════════════════════════════════════════════════════════════════════════
// SWEEP COLLISION
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Perform deterministic swept collision detection
* Breaks movement into adaptive steps to prevent tunneling
* Handles multiple collision iterations for smooth sliding
*
* @param StartLocation - Starting position for sweep
* @param DesiredDelta - Desired movement vector
* @param CapsuleComponent - Capsule for collision shape
* @param Config - Movement configuration with step sizes and iteration limits
* @param DeltaTime - Frame delta time for adaptive step calculation
* @param IsShowVisualDebug - Whether to draw debug traces in world
* @returns SweepResult with final location and collision info
*
* @example
* const result = CollisionResolver.PerformSweep(
* characterLocation,
* new Vector(100, 0, 0), // Move 1 meter forward
* capsuleComponent,
* config,
* 0.016
* );
* character.SetActorLocation(result.Location);
*
* @impure true - performs world traces
* @category Sweep Collision
*/
public PerformSweep(
StartLocation: Vector = new Vector(0, 0, 0),
DesiredDelta: Vector = new Vector(0, 0, 0),
CapsuleComponent: CapsuleComponent | null = null,
Config: DA_MovementConfig = new DA_MovementConfig(),
DeltaTime: Float = 0,
IsShowVisualDebug: boolean = false
): S_SweepResult {
// Validate capsule component
if (SystemLibrary.IsValid(CapsuleComponent)) {
// Calculate total distance to travel
const totalDistance = MathLibrary.VectorLength(DesiredDelta);
// Early exit if movement is negligible
if (totalDistance >= 0.01) {
// Calculate adaptive step size based on velocity
const stepSize = this.CalculateStepSize(
new Vector(
DesiredDelta.X / DeltaTime,
DesiredDelta.Y / DeltaTime,
DesiredDelta.Z / DeltaTime
),
DeltaTime,
Config
);
// Perform stepped sweep
let currentLocation = StartLocation;
let remainingDistance = totalDistance;
let collisionCount = 0;
let lastHit = new HitResult();
// Calculate number of steps (capped by max collision checks)
const CalculateNumSteps = (maxCollisionChecks: Integer): Integer =>
MathLibrary.Min(
MathLibrary.Ceil(totalDistance / stepSize),
maxCollisionChecks
);
for (let i = 0; i < CalculateNumSteps(Config.MaxCollisionChecks); i++) {
collisionCount++;
// Calculate step distance (last step might be shorter)
const currentStepSize = MathLibrary.Min(stepSize, remainingDistance);
const MathExpression = (desiredDelta: Vector): Vector =>
new Vector(
currentLocation.X +
MathLibrary.Normal(desiredDelta).X * currentStepSize,
currentLocation.Y +
MathLibrary.Normal(desiredDelta).Y * currentStepSize,
currentLocation.Z +
MathLibrary.Normal(desiredDelta).Z * currentStepSize
);
// Calculate target position for this step
const targetLocation = MathExpression(DesiredDelta);
// Perform capsule trace for this step
const { OutHit, ReturnValue } = SystemLibrary.CapsuleTraceByChannel(
currentLocation,
targetLocation,
CapsuleComponent.GetScaledCapsuleRadius(),
CapsuleComponent.GetScaledCapsuleHalfHeight(),
ETraceTypeQuery.Visibility,
false,
[],
IsShowVisualDebug
? EDrawDebugTrace.ForDuration
: EDrawDebugTrace.None
);
// Check if trace hit something
if (ReturnValue) {
// Collision detected - return hit location
lastHit = OutHit;
return {
Location: lastHit.Location,
Hit: lastHit,
Blocked: true,
CollisionCount: collisionCount,
};
} else {
// No collision - update position and continue
currentLocation = targetLocation;
remainingDistance = remainingDistance - currentStepSize;
// Check if reached destination
if (remainingDistance <= 0.01) {
break;
}
}
}
// Reached destination without blocking hit
return {
Location: currentLocation,
Hit: lastHit,
Blocked: false,
CollisionCount: collisionCount,
};
} else {
return {
Location: StartLocation,
Hit: new HitResult(),
Blocked: false,
CollisionCount: 0,
};
}
} else {
return {
Location: StartLocation,
Hit: new HitResult(),
Blocked: false,
CollisionCount: 0,
};
}
}
// ════════════════════════════════════════════════════════════════════════════════════════
// SURFACE SLIDING
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Project movement vector onto collision surface for sliding
* Removes component of movement that goes into surface
* Allows character to slide smoothly along walls
*
* @param MovementDelta - Desired movement vector
* @param SurfaceNormal - Normal of surface that was hit
* @returns Projected movement vector parallel to surface
*
* @example
* // Character hits wall at 45° angle
* const slideVector = CollisionResolver.ProjectOntoSurface(
* new Vector(100, 100, 0), // Moving diagonally
* new Vector(-0.707, 0, 0.707) // Wall normal (45° wall)
* );
* // Returns vector parallel to wall surface
*
* @pure true
* @category Surface Sliding
*/
public ProjectOntoSurface(
MovementDelta: Vector,
SurfaceNormal: Vector
): Vector {
// Project by removing normal component
// Formula: V' = V - (V·N)N
const MathExpression = (
movementDelta: Vector,
surfaceNormal: Vector
): Vector =>
new Vector(
MovementDelta.X -
SurfaceNormal.X * MathLibrary.Dot(movementDelta, surfaceNormal),
MovementDelta.Y -
SurfaceNormal.Y * MathLibrary.Dot(movementDelta, surfaceNormal),
MovementDelta.Z -
SurfaceNormal.Z * MathLibrary.Dot(movementDelta, surfaceNormal)
);
return MathExpression(MovementDelta, SurfaceNormal);
}
/**
* Calculate sliding vector after collision
* Combines sweep result with projection for smooth sliding
*
* @param SweepResult - Result from PerformSweep
* @param OriginalDelta - Original desired movement
* @param StartLocation - Starting location before sweep
* @returns Vector to apply for sliding movement
*
* @example
* const slideVector = CollisionResolver.CalculateSlideVector(
* sweepResult,
* desiredDelta,
* startLocation
* );
* if (slideVector.Length() > 0.01) {
* character.SetActorLocation(sweepResult.Location + slideVector);
* }
*
* @pure true
* @category Surface Sliding
*/
public CalculateSlideVector(
SweepResult: S_SweepResult,
OriginalDelta: Vector,
StartLocation: Vector
): Vector {
if (SweepResult.Blocked) {
const MathExpression = (
sweepLocation: Vector,
startLocation: Vector,
originalDelta: Vector
): Vector =>
new Vector(
originalDelta.X - (sweepLocation.X - startLocation.X),
originalDelta.Y - (sweepLocation.Y - startLocation.Y),
originalDelta.Z - (sweepLocation.Z - startLocation.Z)
);
// Project remaining movement onto collision surface
return this.ProjectOntoSurface(
MathExpression(SweepResult.Location, StartLocation, OriginalDelta),
SweepResult.Hit.ImpactNormal
);
} else {
// No sliding if no collision
return new Vector(0, 0, 0);
}
}
// ════════════════════════════════════════════════════════════════════════════════════════
// ADAPTIVE STEPPING
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Calculate adaptive step size based on velocity
* Fast movement = smaller steps (more precise)
* Slow movement = larger steps (more performant)
*
* @param Velocity - Current movement velocity
* @param DeltaTime - Frame delta time
* @param Config - Movement configuration with min/max step sizes
* @returns Step size in cm, clamped between min and max
*
* @example
* const stepSize = CollisionResolver.CalculateStepSize(
* new Vector(1000, 0, 0), // Fast movement
* 0.016,
* config
* );
* // Returns small step size for precise collision detection
*
* @pure true
* @category Adaptive Stepping
*/
public CalculateStepSize(
Velocity: Vector = new Vector(0, 0, 0),
DeltaTime: Float = 0,
Config: DA_MovementConfig = new DA_MovementConfig()
): Float {
// Calculate distance traveled this frame
const frameDistance =
MathLibrary.VectorLength(
new Vector(Velocity.X, Velocity.Y, 0) // Horizontal distance only
) * DeltaTime;
// If moving very slowly, use max step size
if (frameDistance < Config.MinStepSize) {
return Config.MaxStepSize;
}
// Clamp between min and max
return MathLibrary.ClampFloat(
// Calculate adaptive step size (half of frame distance)
// This ensures at least 2 checks per frame
frameDistance * 0.5,
Config.MinStepSize,
Config.MaxStepSize
);
}
}
export const BFL_CollisionResolver = new BFL_CollisionResolverClass();

Binary file not shown.

View File

@ -1,221 +0,0 @@
// Movement/Collision/BFL_GroundProbe.ts
import { DA_MovementConfig } from '#root/Movement/Core/DA_MovementConfig.ts';
import { BFL_SurfaceClassifier } from '#root/Movement/Surface/BFL_SurfaceClassifier.ts';
import { E_SurfaceType } from '#root/Movement/Surface/E_SurfaceType.ts';
import type { S_AngleThresholds } from '#root/Movement/Surface/S_AngleThresholds.ts';
import { BlueprintFunctionLibrary } from '#root/UE/BlueprintFunctionLibrary.ts';
import type { CapsuleComponent } from '#root/UE/CapsuleComponent.ts';
import { EDrawDebugTrace } from '#root/UE/EDrawDebugTrace.ts';
import { ETraceTypeQuery } from '#root/UE/ETraceTypeQuery.ts';
import type { Float } from '#root/UE/Float.ts';
import { HitResult } from '#root/UE/HitResult.ts';
import { MathLibrary } from '#root/UE/MathLibrary.ts';
import { SystemLibrary } from '#root/UE/SystemLibrary.ts';
import { Vector } from '#root/UE/Vector.ts';
class BFL_GroundProbeClass extends BlueprintFunctionLibrary {
// ════════════════════════════════════════════════════════════════════════════════════════
// GROUND DETECTION
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Check if character is standing on walkable ground
* Performs line trace downward from capsule bottom
* Validates surface type using SurfaceClassifier
*
* @param CharacterLocation - Current character world location
* @param CapsuleComponent - Character's capsule component for trace setup
* @param AngleThresholdsRads - Surface angle thresholds in radians
* @param Config - Movement configuration with trace distance
* @param IsShowVisualDebug - Whether to draw debug trace in world
* @returns HitResult with ground information, or empty hit if not grounded
*
* @example
* const groundHit = GroundProbe.CheckGround(
* characterLocation,
* capsuleComponent,
* angleThresholdsRads,
* config
* );
* if (groundHit.BlockingHit) {
* // Character is on walkable ground
* }
*
* @impure true - performs world trace
* @category Ground Detection
*/
public CheckGround(
CharacterLocation: Vector = new Vector(0, 0, 0),
CapsuleComponent: CapsuleComponent | null = null,
AngleThresholdsRads: S_AngleThresholds = {
Walkable: 0,
SteepSlope: 0,
Wall: 0,
},
Config: DA_MovementConfig = new DA_MovementConfig(),
IsShowVisualDebug: boolean = false
): HitResult {
if (SystemLibrary.IsValid(CapsuleComponent)) {
const CalculateEndLocation = (
currentZ: Float,
halfHeight: Float,
groundTraceDistance: Float
): Float => currentZ - halfHeight - groundTraceDistance;
const { OutHit: groundHit, ReturnValue } =
SystemLibrary.LineTraceByChannel(
CharacterLocation,
new Vector(
CharacterLocation.X,
CharacterLocation.Y,
CalculateEndLocation(
CharacterLocation.Z,
CapsuleComponent.GetScaledCapsuleHalfHeight(),
Config.GroundTraceDistance
)
),
ETraceTypeQuery.Visibility,
false,
[],
IsShowVisualDebug ? EDrawDebugTrace.ForDuration : EDrawDebugTrace.None
);
// Check if trace hit something
if (!ReturnValue) {
return new HitResult();
}
if (
BFL_SurfaceClassifier.IsWalkable(
BFL_SurfaceClassifier.Classify(
groundHit.ImpactNormal,
AngleThresholdsRads
)
)
) {
return groundHit;
} else {
return new HitResult();
}
} else {
return new HitResult();
}
}
// ════════════════════════════════════════════════════════════════════════════════════════
// GROUND SNAPPING
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Calculate snapped location to keep character on ground
* Prevents character from floating slightly above ground
* Only snaps if within reasonable distance threshold
*
* @param CurrentLocation - Current character location
* @param GroundHit - Ground hit result from CheckGround
* @param CapsuleComponent - Character's capsule component
* @param SnapThreshold - Maximum distance to snap (default: 10 cm)
* @returns Snapped location or original location if too far
*
* @example
* const snappedLocation = GroundProbe.CalculateSnapLocation(
* currentLocation,
* groundHit,
* capsuleComponent,
* 10.0
* );
* character.SetActorLocation(snappedLocation);
*
* @pure true - only calculations, no side effects
* @category Ground Snapping
*/
public CalculateSnapLocation(
CurrentLocation: Vector,
GroundHit: HitResult,
CapsuleComponent: CapsuleComponent | null,
SnapThreshold: Float = 10.0
): Vector {
if (GroundHit.BlockingHit) {
if (SystemLibrary.IsValid(CapsuleComponent)) {
const correctZ =
GroundHit.Location.Z + CapsuleComponent.GetScaledCapsuleHalfHeight();
const CalculateZDifference = (currentLocZ: Float): Float =>
MathLibrary.abs(currentLocZ - correctZ);
const zDifference = CalculateZDifference(CurrentLocation.Z);
const ShouldSnap = (groundTraceDistance: Float): boolean =>
zDifference > 0.1 && zDifference < groundTraceDistance;
if (ShouldSnap(SnapThreshold)) {
return new Vector(CurrentLocation.X, CurrentLocation.Y, correctZ);
} else {
return CurrentLocation;
}
} else {
return CurrentLocation;
}
} else {
return CurrentLocation;
}
}
/**
* Check if ground snapping should be applied
* Helper method to determine if conditions are right for snapping
*
* @param CurrentVelocityZ - Current vertical velocity
* @param GroundHit - Ground hit result
* @param IsGrounded - Whether character is considered grounded
* @returns True if snapping should be applied
*
* @example
* if (GroundProbe.ShouldSnapToGround(velocity.Z, groundHit, isGrounded)) {
* const snappedLoc = GroundProbe.CalculateSnapLocation(...);
* character.SetActorLocation(snappedLoc);
* }
*
* @pure true
* @category Ground Snapping
*/
public ShouldSnapToGround(
CurrentVelocityZ: Float,
GroundHit: HitResult,
IsGrounded: boolean
): boolean {
return IsGrounded && GroundHit.BlockingHit && CurrentVelocityZ <= 0;
}
// ════════════════════════════════════════════════════════════════════════════════════════
// UTILITIES
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Get surface type from ground hit
* Convenience method combining trace result with classification
*
* @param GroundHit - Ground hit result
* @param AngleThresholdsRads - Surface angle thresholds in radians
* @returns Surface type classification
*
* @pure true
* @category Utilities
*/
public GetSurfaceType(
GroundHit: HitResult,
AngleThresholdsRads: S_AngleThresholds
): E_SurfaceType {
if (!GroundHit.BlockingHit) {
return BFL_SurfaceClassifier.Classify(
GroundHit.ImpactNormal,
AngleThresholdsRads
);
} else {
return E_SurfaceType.None;
}
}
}
export const BFL_GroundProbe = new BFL_GroundProbeClass();

Binary file not shown.

View File

@ -1,12 +0,0 @@
// Movement/Collision/S_SweepResult.ts
import type { HitResult } from '#root/UE/HitResult.ts';
import type { Integer } from '#root/UE/Integer.ts';
import type { Vector } from '#root/UE/Vector.ts';
export interface S_SweepResult {
Location: Vector;
Hit: HitResult;
Blocked: boolean;
CollisionCount: Integer;
}

Binary file not shown.

View File

@ -1,261 +0,0 @@
// Movement/Core/BFL_MovementProcessor.ts
import { BFL_CollisionResolver } from '#root/Movement/Collision/BFL_CollisionResolver.ts';
import { BFL_GroundProbe } from '#root/Movement/Collision/BFL_GroundProbe.ts';
import { E_MovementState } from '#root/Movement/Core/E_MovementState.ts';
import type { S_MovementInput } from '#root/Movement/Core/S_MovementInput.ts';
import type { S_MovementState } from '#root/Movement/Core/S_MovementState.ts';
import { BFL_Kinematics } from '#root/Movement/Physics/BFL_Kinematics.ts';
import { BFL_RotationController } from '#root/Movement/Rotation/BFL_RotationController.ts';
import { BFL_MovementStateMachine } from '#root/Movement/State/BFL_MovementStateMachine.ts';
import { BFL_SurfaceClassifier } from '#root/Movement/Surface/BFL_SurfaceClassifier.ts';
import { E_SurfaceType } from '#root/Movement/Surface/E_SurfaceType.ts';
import { BlueprintFunctionLibrary } from '#root/UE/BlueprintFunctionLibrary.ts';
import { HitResult } from '#root/UE/HitResult.ts';
import { MathLibrary } from '#root/UE/MathLibrary.ts';
import type { Rotator } from '#root/UE/Rotator.ts';
import { Vector } from '#root/UE/Vector.ts';
/**
* Movement Processor
*
* Unified movement processing system
* Takes current state + input, returns next state
* Pure functional approach - no side effects
*
* @category Movement Processing
* @impure Only collision traces (GroundProbe, CollisionResolver)
*/
class BFL_MovementProcessorClass extends BlueprintFunctionLibrary {
// ════════════════════════════════════════════════════════════════════════════════════════
// MAIN PROCESSING
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Process movement for one frame
*
* Main entry point - computes complete next state from current state + input
* Orchestrates all movement subsystems in correct order
*
* @param CurrentState - Current movement state
* @param Input - Movement input data (input vector, delta time, config, etc.)
* @param IsShowVisualDebug - Whether to show debug traces in the world
* @returns New movement state after processing
*
* @example
* const newState = BFL_MovementProcessor.ProcessMovement(
* this.CurrentMovementState,
* {
* InputVector: inputVector,
* DeltaTime: deltaTime,
* CapsuleComponent: this.CapsuleComponent,
* Config: this.Config,
* AngleThresholdsRads: this.AngleThresholdsRads
* }
* );
*
* // Apply results
* this.GetOwner().SetActorLocation(newState.Location);
* this.GetOwner().SetActorRotation(newState.Rotation);
*
* @impure true - performs collision traces
* @category Main Processing
*/
public ProcessMovement(
CurrentState: S_MovementState,
Input: S_MovementInput,
IsShowVisualDebug: boolean = false
): S_MovementState {
// ═══════════════════════════════════════════════════════════════════
// PHASE 1: INPUT & ROTATION
// ═══════════════════════════════════════════════════════════════════
const inputMagnitude = MathLibrary.VectorLength(Input.InputVector);
const rotationResult = BFL_RotationController.UpdateRotation(
CurrentState.Rotation,
Input.InputVector,
Input.Config,
Input.DeltaTime,
CurrentState.Speed
);
// ═══════════════════════════════════════════════════════════════════
// PHASE 2: GROUND DETECTION
// ═══════════════════════════════════════════════════════════════════
const groundHit = BFL_GroundProbe.CheckGround(
CurrentState.Location,
Input.CapsuleComponent,
Input.AngleThresholdsRads,
Input.Config
);
const isGrounded = groundHit.BlockingHit;
const surfaceType = BFL_GroundProbe.GetSurfaceType(
groundHit,
Input.AngleThresholdsRads
);
// ═══════════════════════════════════════════════════════════════════
// PHASE 3: PHYSICS CALCULATION
// ═══════════════════════════════════════════════════════════════════
let newVelocity = CurrentState.Velocity;
// Ground movement or air friction
if (BFL_SurfaceClassifier.IsWalkable(surfaceType) && isGrounded) {
newVelocity = BFL_Kinematics.CalculateGroundVelocity(
newVelocity,
Input.InputVector,
Input.DeltaTime,
Input.Config
);
} else {
newVelocity = BFL_Kinematics.CalculateFriction(
newVelocity,
Input.DeltaTime,
Input.Config
);
}
// Apply gravity
newVelocity = BFL_Kinematics.CalculateGravity(
newVelocity,
isGrounded,
Input.Config
);
const newSpeed = BFL_Kinematics.GetHorizontalSpeed(newVelocity);
// ═══════════════════════════════════════════════════════════════════
// PHASE 4: MOVEMENT APPLICATION (Sweep)
// ═══════════════════════════════════════════════════════════════════
const desiredDelta = new Vector(
newVelocity.X * Input.DeltaTime,
newVelocity.Y * Input.DeltaTime,
newVelocity.Z * Input.DeltaTime
);
const sweepResult = BFL_CollisionResolver.PerformSweep(
CurrentState.Location,
desiredDelta,
Input.CapsuleComponent,
Input.Config,
Input.DeltaTime,
IsShowVisualDebug
);
let finalLocation = sweepResult.Location;
// Handle collision sliding
if (sweepResult.Blocked) {
const slideVector = BFL_CollisionResolver.CalculateSlideVector(
sweepResult,
desiredDelta,
CurrentState.Location
);
if (
MathLibrary.VectorLength(slideVector) > 0.5 &&
MathLibrary.Dot(
MathLibrary.Normal(slideVector),
sweepResult.Hit.ImpactNormal
) >= -0.1
) {
finalLocation = new Vector(
sweepResult.Location.X + slideVector.X,
sweepResult.Location.Y + slideVector.Y,
sweepResult.Location.Z + slideVector.Z
);
}
}
// ═══════════════════════════════════════════════════════════════════
// PHASE 5: GROUND SNAPPING
// ═══════════════════════════════════════════════════════════════════
if (
BFL_GroundProbe.ShouldSnapToGround(newVelocity.Z, groundHit, isGrounded)
) {
finalLocation = BFL_GroundProbe.CalculateSnapLocation(
finalLocation,
groundHit,
Input.CapsuleComponent,
Input.Config.GroundTraceDistance
);
}
// ═══════════════════════════════════════════════════════════════════
// PHASE 6: STATE DETERMINATION
// ═══════════════════════════════════════════════════════════════════
const movementState = BFL_MovementStateMachine.DetermineState({
IsGrounded: isGrounded,
SurfaceType: surfaceType,
InputMagnitude: inputMagnitude,
CurrentSpeed: newSpeed,
VerticalVelocity: newVelocity.Z,
IsBlocked: sweepResult.Blocked,
});
// ═══════════════════════════════════════════════════════════════════
// RETURN NEW STATE
// ═══════════════════════════════════════════════════════════════════
return {
Location: finalLocation,
Rotation: rotationResult.Rotation,
Velocity: newVelocity,
Speed: newSpeed,
IsGrounded: isGrounded,
GroundHit: groundHit,
SurfaceType: surfaceType,
IsBlocked: sweepResult.Blocked,
CollisionCount: sweepResult.CollisionCount,
IsRotating: rotationResult.IsRotating,
RotationDelta: rotationResult.RemainingDelta,
MovementState: movementState,
InputMagnitude: inputMagnitude,
};
}
// ════════════════════════════════════════════════════════════════════════════════════════
// STATE UTILITIES
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Create initial movement state
*
* @param Location - Starting location
* @param Rotation - Starting rotation
* @returns Initial movement state with defaults
*
* @pure true
* @category State Utilities
*/
public CreateInitialState(
Location: Vector,
Rotation: Rotator
): S_MovementState {
return {
Location,
Rotation,
Velocity: new Vector(0, 0, 0),
Speed: 0.0,
IsGrounded: false,
GroundHit: new HitResult(),
SurfaceType: E_SurfaceType.None,
IsBlocked: false,
CollisionCount: 0,
IsRotating: false,
RotationDelta: 0.0,
MovementState: E_MovementState.Idle,
InputMagnitude: 0.0,
};
}
}
export const BFL_MovementProcessor = new BFL_MovementProcessorClass();

Binary file not shown.

View File

@ -1,149 +0,0 @@
// Movement/Core/DA_MovementConfig.ts
import { S_AngleThresholds } from '#root/Movement/Surface/S_AngleThresholds.ts';
import type { Float } from '#root/UE/Float.ts';
import { PrimaryDataAsset } from '#root/UE/PrimaryDataAsset.ts';
export class DA_MovementConfig extends PrimaryDataAsset {
// ════════════════════════════════════════════════════════════════════════════════════════
// MOVEMENT PHYSICS
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Maximum horizontal movement speed in UE units per second
* Character cannot exceed this speed through ground movement
* Used as target velocity cap in ProcessGroundMovement
*
* @category Movement Physics
* @instanceEditable true
* @unit cm/s
*/
public readonly MaxSpeed: Float = 800.0;
/**
* Speed of velocity interpolation towards target velocity
* Higher values = faster acceleration, more responsive feel
* Used with VInterpTo for smooth acceleration curves
* Value represents interpolation speed, not actual acceleration rate
*
* @category Movement Physics
* @instanceEditable true
*/
public readonly Acceleration: Float = 10.0;
/**
* Speed of velocity interpolation towards zero when no input
* Higher values = faster stopping, less sliding
* Used with VInterpTo for smooth deceleration curves
* Should typically be <= Acceleration for natural feel
*
* @category Movement Physics
* @instanceEditable true
*/
public readonly Friction: Float = 8.0;
/**
* Gravitational acceleration in UE units per second squared
* Applied to vertical velocity when character is airborne
* Standard Earth gravity 980 cm/s² in UE units
* Only affects Z-axis velocity, horizontal movement unaffected
*
* @category Movement Physics
* @instanceEditable true
* @unit cm/s^2
*/
public readonly Gravity: Float = 980.0;
// ════════════════════════════════════════════════════════════════════════════════════════
// SURFACE DETECTION
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Surface classification angle thresholds in degrees
* Walkable 50°, SteepSlope 85°, Wall 95°, Ceiling >95°
*
* @category Surface Detection
* @instanceEditable true
*/
public readonly AngleThresholdsDegrees: S_AngleThresholds = {
Walkable: 50,
SteepSlope: 85,
Wall: 95,
};
// ════════════════════════════════════════════════════════════════════════════════════════
// COLLISION SETTINGS
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Distance to trace downward for ground detection
* Should be slightly larger than capsule half-height
*
* @category Collision Settings
* @instanceEditable true
* @unit cm
*/
public readonly GroundTraceDistance: Float = 50.0;
/**
* Minimum step size for collision sweeps
* Smaller values = more precise but more expensive
*
* @category Collision Settings
* @instanceEditable true
* @unit cm
*/
public readonly MinStepSize: Float = 1.0;
/**
* Maximum step size for collision sweeps
* Larger values = less precise but cheaper
*
* @category Collision Settings
* @instanceEditable true
* @unit cm
*/
public readonly MaxStepSize: Float = 50.0;
/**
* Maximum collision checks allowed per frame
* Prevents infinite loops in complex geometry
*
* @category Collision Settings
* @instanceEditable true
*/
public readonly MaxCollisionChecks: Float = 25;
// ════════════════════════════════════════════════════════════════════════════════════════
// CHARACTER ROTATION
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Character rotation speed (degrees per second)
* How fast character turns toward movement direction
*
* @category Character Rotation
* @instanceEditable true
* @unit deg/s
*/
public RotationSpeed: Float = 360.0;
/**
* Minimum movement speed required to rotate character
* Prevents rotation jitter when nearly stationary
*
* @category Character Rotation
* @instanceEditable true
* @unit cm/s
*/
public MinSpeedForRotation: Float = 50.0;
/**
* Enable/disable character rotation toward movement
* Useful for debugging or special movement modes
*
* @category Character Rotation
* @instanceEditable true
*/
public ShouldRotateToMovement: boolean = true;
}

Binary file not shown.

View File

@ -1,41 +0,0 @@
// Movement/Core/DA_MovementConfigDefault.ts
import { DA_MovementConfig } from '#root/Movement/Core/DA_MovementConfig.ts';
export class DA_MovementConfigDefault extends DA_MovementConfig {
// ════════════════════════════════════════════════════════════════════════════════════════
// MOVEMENT PHYSICS
// ════════════════════════════════════════════════════════════════════════════════════════
override MaxSpeed = 800.0;
override Acceleration = 10.0;
override Friction = 8.0;
override Gravity = 980.0;
// ════════════════════════════════════════════════════════════════════════════════════════
// SURFACE DETECTION
// ════════════════════════════════════════════════════════════════════════════════════════
override AngleThresholdsDegrees = {
Walkable: 50.0,
SteepSlope: 85.0,
Wall: 95.0,
};
// ════════════════════════════════════════════════════════════════════════════════════════
// COLLISION SETTINGS
// ════════════════════════════════════════════════════════════════════════════════════════
override GroundTraceDistance = 50.0;
override MinStepSize = 1.0;
override MaxStepSize = 50.0;
override MaxCollisionChecks = 25;
// ════════════════════════════════════════════════════════════════════════════════════════
// CHARACTER ROTATION
// ════════════════════════════════════════════════════════════════════════════════════════
override RotationSpeed = 360.0;
override MinSpeedForRotation = 50.0;
override ShouldRotateToMovement = true;
}

Binary file not shown.

View File

@ -1,39 +0,0 @@
// Movement/Core/E_MovementState.ts
/**
* Movement state enumeration
* Defines all possible character movement states
*
* @category Movement Enums
*/
export enum E_MovementState {
/**
* Character is stationary on ground
* No input, no movement
*/
Idle = 'Idle',
/**
* Character is moving on ground
* Has input and horizontal velocity
*/
Walking = 'Walking',
/**
* Character is in the air
* Not touching ground, affected by gravity
*/
Airborne = 'Airborne',
/**
* Character is sliding down steep slope
* On non-walkable surface (steep slope)
*/
Sliding = 'Sliding',
/**
* Character is blocked by collision
* Hitting wall or ceiling
*/
Blocked = 'Blocked',
}

Binary file not shown.

View File

@ -1,40 +0,0 @@
// Movement/Core/S_MovementInput.ts
import type { DA_MovementConfig } from '#root/Movement/Core/DA_MovementConfig.ts';
import type { S_AngleThresholds } from '#root/Movement/Surface/S_AngleThresholds.ts';
import type { CapsuleComponent } from '#root/UE/CapsuleComponent.ts';
import type { Float } from '#root/UE/Float.ts';
import type { Vector } from '#root/UE/Vector.ts';
/**
* Movement processing input data
* All data needed to compute next movement state
*
* @category Movement Input
*/
export interface S_MovementInput {
/**
* Player input vector (normalized XY direction)
*/
InputVector: Vector;
/**
* Frame delta time (seconds)
*/
DeltaTime: Float;
/**
* Character capsule component for collision
*/
CapsuleComponent: CapsuleComponent | null;
/**
* Movement configuration
*/
Config: DA_MovementConfig;
/**
* Angle thresholds in radians (for surface classification)
*/
AngleThresholdsRads: S_AngleThresholds;
}

Binary file not shown.

View File

@ -1,105 +0,0 @@
// Movement/Core/S_MovementState.ts
import type { E_MovementState } from '#root/Movement/Core/E_MovementState.ts';
import type { E_SurfaceType } from '#root/Movement/Surface/E_SurfaceType.ts';
import type { Float } from '#root/UE/Float.ts';
import type { HitResult } from '#root/UE/HitResult.ts';
import type { Rotator } from '#root/UE/Rotator.ts';
import type { Vector } from '#root/UE/Vector.ts';
/**
* Complete movement state snapshot
* Immutable data structure representing full character movement state
*
* @category Movement State
*/
export interface S_MovementState {
// ═══════════════════════════════════════════════════════════════════
// TRANSFORM
// ═══════════════════════════════════════════════════════════════════
/**
* Character world location
*/
Location: Vector;
/**
* Character rotation (yaw only)
*/
Rotation: Rotator;
// ═══════════════════════════════════════════════════════════════════
// VELOCITY & PHYSICS
// ═══════════════════════════════════════════════════════════════════
/**
* Current velocity vector (cm/s)
*/
Velocity: Vector;
/**
* Horizontal speed (cm/s)
*/
Speed: Float;
// ═══════════════════════════════════════════════════════════════════
// GROUND STATE
// ═══════════════════════════════════════════════════════════════════
/**
* Whether character is on walkable ground
*/
IsGrounded: boolean;
/**
* Ground trace hit result
*/
GroundHit: HitResult;
/**
* Current surface type
*/
SurfaceType: E_SurfaceType;
// ═══════════════════════════════════════════════════════════════════
// COLLISION STATE
// ═══════════════════════════════════════════════════════════════════
/**
* Whether movement was blocked by collision
*/
IsBlocked: boolean;
/**
* Number of collision checks this frame
*/
CollisionCount: number;
// ═══════════════════════════════════════════════════════════════════
// ROTATION STATE
// ═══════════════════════════════════════════════════════════════════
/**
* Whether character is actively rotating
*/
IsRotating: boolean;
/**
* Remaining angular distance to target (degrees)
*/
RotationDelta: Float;
// ═══════════════════════════════════════════════════════════════════
// MOVEMENT STATE
// ═══════════════════════════════════════════════════════════════════
/**
* Current movement state (Idle, Walking, Airborne, etc.)
*/
MovementState: E_MovementState;
/**
* Input magnitude (0-1)
*/
InputMagnitude: Float;
}

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.

File diff suppressed because it is too large Load Diff

View File

@ -1,179 +0,0 @@
// Movement/Physics/BFL_Kinematics.ts
import type { DA_MovementConfig } from '#root/Movement/Core/DA_MovementConfig.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';
class BFL_KinematicsClass extends BlueprintFunctionLibrary {
// ════════════════════════════════════════════════════════════════════════════════════════
// GROUND MOVEMENT
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Calculate new velocity for ground-based movement with acceleration
* Uses VInterpTo for smooth acceleration towards target velocity
* Only affects horizontal (XY) components, preserves vertical (Z)
*
* @param CurrentVelocity - Current character velocity (cm/s)
* @param InputVector - Normalized input direction from player/AI
* @param DeltaTime - Frame delta time for frame-rate independence (s)
* @param Config - Movement configuration with MaxSpeed and Acceleration
* @returns New velocity vector with updated horizontal components
*
* @example
* // Character moving forward with input (1, 0, 0)
* const newVel = Kinematics.CalculateGroundVelocity(
* new Vector(400, 0, 0), // Current velocity
* new Vector(1, 0, 0), // Forward input
* 0.016, // 60 FPS delta
* config
* );
* // Returns: Vector(450, 0, 0) - accelerated towards MaxSpeed
*
* @pure true
* @category Ground Movement
*/
public CalculateGroundVelocity(
CurrentVelocity: Vector,
InputVector: Vector,
DeltaTime: Float,
Config: DA_MovementConfig
): Vector {
if (MathLibrary.VectorLength(InputVector) > 0.01) {
const CalculateTargetVelocity = (
inputVector: Vector,
maxSpeed: Float
): Vector =>
new Vector(
MathLibrary.Normal(inputVector).X * maxSpeed,
MathLibrary.Normal(inputVector).Y * maxSpeed,
MathLibrary.Normal(inputVector).Z * maxSpeed
);
return MathLibrary.VInterpTo(
new Vector(CurrentVelocity.X, CurrentVelocity.Y, 0),
CalculateTargetVelocity(InputVector, Config.MaxSpeed),
DeltaTime,
Config.Acceleration
);
} else {
return this.CalculateFriction(CurrentVelocity, DeltaTime, Config);
}
}
// ════════════════════════════════════════════════════════════════════════════════════════
// FRICTION
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Apply friction to horizontal velocity (deceleration when no input)
* Smoothly interpolates velocity towards zero using friction rate
* Only affects horizontal (XY) components, preserves vertical (Z)
*
* @param CurrentVelocity - Current character velocity (cm/s)
* @param DeltaTime - Frame delta time (s)
* @param Config - Movement configuration with Friction rate
* @returns New velocity vector with friction applied to horizontal components
*
* @example
* // Character sliding to stop after input released
* const newVel = Kinematics.ApplyFriction(
* new Vector(500, 0, 0), // Moving forward
* 0.016, // 60 FPS delta
* config // Friction = 8.0
* );
* // Returns: Vector(450, 0, 0) - smoothly decelerating
*
* @pure true
* @category Friction
*/
public CalculateFriction(
CurrentVelocity: Vector,
DeltaTime: Float,
Config: DA_MovementConfig
): Vector {
return MathLibrary.VInterpTo(
new Vector(CurrentVelocity.X, CurrentVelocity.Y, 0),
new Vector(0, 0, CurrentVelocity.Z),
DeltaTime,
Config.Friction
);
}
// ════════════════════════════════════════════════════════════════════════════════════════
// GRAVITY
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Apply gravity to vertical velocity when airborne
* Only affects Z component, horizontal velocity unchanged
* Gravity is NOT applied when grounded (Z velocity set to 0)
*
* @param CurrentVelocity - Current character velocity (cm/s)
* @param IsGrounded - Whether character is on walkable surface
* @param Config - Movement configuration with Gravity force
* @returns New velocity vector with gravity applied to vertical component
*
* @example
* // Character falling (not grounded)
* const newVel = Kinematics.ApplyGravity(
* new Vector(500, 0, -200), // Moving forward and falling
* false, // Not grounded
* config // Gravity = 980 cm/s²
* );
* // Returns: Vector(500, 0, -216.8) - falling faster
*
* @example
* // Character on ground
* const newVel = Kinematics.ApplyGravity(
* new Vector(500, 0, -10), // Small downward velocity
* true, // Grounded
* config
* );
* // Returns: Vector(500, 0, 0) - vertical velocity zeroed
*
* @pure true
* @category Gravity
*/
public CalculateGravity(
CurrentVelocity: Vector,
IsGrounded: boolean,
Config: DA_MovementConfig
): Vector {
if (!IsGrounded) {
return new Vector(
CurrentVelocity.X,
CurrentVelocity.Y,
CurrentVelocity.Z - Config.Gravity
);
} else {
return new Vector(CurrentVelocity.X, CurrentVelocity.Y, 0);
}
}
// ════════════════════════════════════════════════════════════════════════════════════════
// VELOCITY QUERIES
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Get horizontal speed (magnitude of XY velocity)
* Ignores vertical component, useful for animation and debug display
*
* @param Velocity - Velocity vector to measure
* @returns Speed in cm/s (horizontal plane only)
*
* @example
* const speed = Kinematics.GetHorizontalSpeed(new Vector(300, 400, -100));
* // Returns: 500.0 (sqrt(300² + 400²))
*
* @pure true
* @category Velocity Queries
*/
public GetHorizontalSpeed(Velocity: Vector): Float {
return MathLibrary.VectorLength(new Vector(Velocity.X, Velocity.Y, 0));
}
}
export const BFL_Kinematics = new BFL_KinematicsClass();

Binary file not shown.

View File

@ -1,256 +0,0 @@
// Movement/Rotation/BFL_RotationController.ts
import type { DA_MovementConfig } from '#root/Movement/Core/DA_MovementConfig.ts';
import type { S_RotationResult } from '#root/Movement/Rotation/S_RotationResult.ts';
import type { Float } from '#root/UE/Float.ts';
import type { Integer } from '#root/UE/Integer.ts';
import { MathLibrary } from '#root/UE/MathLibrary.ts';
import { Rotator } from '#root/UE/Rotator.ts';
import type { Vector } from '#root/UE/Vector.ts';
/**
* Character Rotation Controller
*
* Pure functional module for character rotation calculations
* Handles smooth rotation toward movement direction
* All methods are deterministic and side-effect free
*
* @category Movement Rotation
* @pure All methods are pure functions
*/
class BFL_RotationControllerClass {
// ════════════════════════════════════════════════════════════════════════════════════════
// TARGET CALCULATION
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Calculate target yaw angle from movement direction
* Converts 2D movement vector to rotation angle
*
* @param MovementDirection - Movement direction vector (XY plane)
* @returns Target yaw angle in degrees
*
* @example
* // Moving forward (X+)
* const yaw = RotationController.CalculateTargetYaw(new Vector(1, 0, 0));
* // Returns: 0°
*
* @example
* // Moving right (Y+)
* const yaw = RotationController.CalculateTargetYaw(new Vector(0, 1, 0));
* // Returns: 90°
*
* @pure true
* @category Target Calculation
*/
public CalculateTargetYaw(MovementDirection: Vector): Float {
// Use atan2 to get angle from X/Y components
// Returns angle in degrees
return MathLibrary.Atan2Degrees(MovementDirection.Y, MovementDirection.X);
}
/**
* Calculate target rotation from movement direction
* Creates full Rotator with only yaw set (pitch/roll = 0)
*
* @param MovementDirection - Movement direction vector
* @returns Target rotation (yaw only, pitch/roll = 0)
*
* @pure true
* @category Target Calculation
*/
public CalculateTargetRotation(MovementDirection: Vector): Rotator {
return new Rotator(0, this.CalculateTargetYaw(MovementDirection), 0);
}
// ════════════════════════════════════════════════════════════════════════════════════════
// ROTATION INTERPOLATION
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Interpolate rotation smoothly toward target
* Handles angle wraparound (180°/-180° boundary)
*
* @param CurrentRotation - Current character rotation
* @param TargetRotation - Desired target rotation
* @param RotationSpeed - Rotation speed in degrees/sec
* @param DeltaTime - Frame delta time
* @param MinSpeedForRotation - Minimum speed to allow rotation (default: 0)
* @param CurrentSpeed - Current movement speed for threshold check
* @returns RotationResult with new rotation and metadata
*
* @example
* const result = RotationController.InterpolateRotation(
* new Rotator(0, 0, 0), // Current: facing forward
* new Rotator(0, 90, 0), // Target: facing right
* 720, // 720°/sec rotation speed
* 0.016, // 60 FPS delta
* 50, // Min speed threshold
* 500 // Current speed
* );
* // Returns: Rotator smoothly interpolated toward 90°
*
* @pure true
* @category Rotation Interpolation
*/
public InterpolateRotation(
CurrentRotation: Rotator,
TargetRotation: Rotator,
RotationSpeed: Float,
DeltaTime: Float,
MinSpeedForRotation: Float = 0.0,
CurrentSpeed: Float = 0.0
): S_RotationResult {
// Check if character is moving fast enough to rotate
if (CurrentSpeed >= MinSpeedForRotation) {
// Calculate angular distance with wraparound handling
const angularDistance = this.GetAngularDistance(
CurrentRotation.yaw,
TargetRotation.yaw
);
// Check if rotation is not complete (within 1° tolerance)
if (MathLibrary.abs(angularDistance) <= 1.0) {
const CalculateNewYaw = (
currentRotationYaw: Float,
rotationDirection: Integer,
rotationSpeed: Float,
deltaTime: Float
): Float =>
currentRotationYaw +
MathLibrary.Min(
rotationSpeed * deltaTime,
MathLibrary.abs(angularDistance)
) *
rotationDirection;
return {
Rotation: new Rotator(
0,
CalculateNewYaw(
CurrentRotation.yaw,
angularDistance > 0 ? -1 : 1,
RotationSpeed,
DeltaTime
),
0
),
IsRotating: true,
RemainingDelta: MathLibrary.abs(angularDistance),
};
} else {
return {
Rotation: TargetRotation,
IsRotating: false,
RemainingDelta: 0.0,
};
}
} else {
return {
Rotation: CurrentRotation,
IsRotating: false,
RemainingDelta: 0.0,
};
}
}
// ════════════════════════════════════════════════════════════════════════════════════════
// ANGLE UTILITIES
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Calculate the shortest angular distance between two angles
* Handles wraparound for shortest path
*
* @param fromAngle - Starting angle in degrees
* @param toAngle - Target angle in degrees
* @returns Signed angular distance (positive = clockwise, negative = counter-clockwise)
*
* @example
* GetAngularDistance(10, 350) // Returns: -20 (shorter to go counter-clockwise)
* GetAngularDistance(350, 10) // Returns: 20 (shorter to go clockwise)
* GetAngularDistance(0, 180) // Returns: 180 (either direction same)
*
* @pure true
* @category Angle Utilities
*/
public GetAngularDistance(fromAngle: Float, toAngle: Float): Float {
// Calculate raw difference
let difference = fromAngle - toAngle;
// Normalize to the shortest path
if (difference > 180) {
difference -= 360;
} else if (difference < -180) {
difference += 360;
}
return difference;
}
// ════════════════════════════════════════════════════════════════════════════════════════
// CONVENIENCE METHODS
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Update character rotation toward movement direction
* Convenience method combining target calculation and interpolation
*
* @param CurrentRotation - Current character rotation
* @param MovementDirection - Movement direction vector
* @param Config - Movement configuration with rotation settings
* @param DeltaTime - Frame delta time
* @param CurrentSpeed - Current movement speed
* @returns RotationResult with updated rotation
*
* @example
* const result = RotationController.UpdateRotation(
* CurrentRotation,
* InputVector,
* Config,
* DeltaTime,
* CurrentSpeed
* );
* character.SetActorRotation(result.Rotation);
*
* @pure true
* @category Convenience Methods
*/
public UpdateRotation(
CurrentRotation: Rotator,
MovementDirection: Vector,
Config: DA_MovementConfig,
DeltaTime: Float,
CurrentSpeed: Float
): S_RotationResult {
// Rotation if enabled in config
if (Config.ShouldRotateToMovement) {
// Rotation if movement
if (MathLibrary.VectorLength(MovementDirection) >= 0.01) {
// Calculate target and interpolate;
return this.InterpolateRotation(
CurrentRotation,
this.CalculateTargetRotation(MovementDirection),
Config.RotationSpeed,
DeltaTime,
Config.MinSpeedForRotation,
CurrentSpeed
);
} else {
return {
Rotation: CurrentRotation,
IsRotating: false,
RemainingDelta: 0.0,
};
}
} else {
return {
Rotation: CurrentRotation,
IsRotating: false,
RemainingDelta: 0.0,
};
}
}
}
export const BFL_RotationController = new BFL_RotationControllerClass();

Binary file not shown.

View File

@ -1,29 +0,0 @@
// Movement/Rotation/S_RotationResult.ts
import type { Float } from '#root/UE/Float.ts';
import type { Rotator } from '#root/UE/Rotator.ts';
/**
* Rotation result data
* Contains updated rotation and metadata about rotation state
*
* @category Movement Rotation
*/
export interface S_RotationResult {
/**
* New rotation after interpolation
*/
Rotation: Rotator;
/**
* Whether character is actively rotating
* False if rotation is complete or speed too low
*/
IsRotating: boolean;
/**
* Angular distance remaining to target (degrees)
* Used for animations and debug
*/
RemainingDelta: Float;
}

Binary file not shown.

View File

@ -1,105 +0,0 @@
// Movement/State/BFL_MovementStateMachine.ts
import { E_MovementState } from '#root/Movement/Core/E_MovementState.ts';
import type { S_MovementContext } from '#root/Movement/State/S_MovementContext.ts';
import { E_SurfaceType } from '#root/Movement/Surface/E_SurfaceType.ts';
/**
* Movement State Machine
*
* Pure functional FSM for determining movement state
* Takes movement context and returns appropriate state
* No side effects - completely deterministic
*
* @category Movement State
* @pure All methods are pure functions
*/
class BFL_MovementStateMachineClass {
// ════════════════════════════════════════════════════════════════════════════════════════
// STATE DETERMINATION
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Determine movement state based on current Context
* Main entry point for state machine logic
*
* @param Context - Current movement context
* @returns Appropriate movement state
*
* @example
* const state = MovementStateMachine.DetermineState({
* IsGrounded: true,
* SurfaceType: E_SurfaceType.Walkable,
* InputMagnitude: 0.8,
* CurrentSpeed: 500,
* VerticalVelocity: 0,
* IsBlocked: false
* });
* // Returns: E_MovementState.Walking
*
* @pure true
* @category State Determination
*/
public DetermineState(Context: S_MovementContext): E_MovementState {
// Priority 1: Check if grounded
if (Context.IsGrounded) {
// Priority 2: Check surface type
if (Context.SurfaceType === E_SurfaceType.SteepSlope) {
return E_MovementState.Sliding;
} else if (
Context.SurfaceType === E_SurfaceType.Wall ||
Context.SurfaceType === E_SurfaceType.Ceiling ||
// Priority 3: Check if blocked by collision
Context.IsBlocked
) {
return E_MovementState.Blocked;
} else {
// Priority 4: Determine ground state based on input
return this.DetermineGroundedState(Context);
}
} else {
return this.DetermineAirborneState(Context);
}
}
// ════════════════════════════════════════════════════════════════════════════════════════
// STATE HELPERS
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Determine state when character is airborne
* Distinguishes between jumping, falling, etc.
*
* @param Context - Current movement context
* @returns Airborne-specific state
*
* @pure true
* @category State Helpers
*/
private DetermineAirborneState(Context: S_MovementContext): E_MovementState {
// Could extend this to differentiate Jump vs Fall
// For now, just return Airborne
return E_MovementState.Airborne;
}
/**
* Determine state when character is on ground
* Distinguishes between idle, walking, running, etc.
*
* @param Context - Current movement context
* @returns Grounded-specific state
*
* @pure true
* @category State Helpers
*/
private DetermineGroundedState(Context: S_MovementContext): E_MovementState {
// Check if player is providing input
if (Context.InputMagnitude > 0.01 && Context.CurrentSpeed > 1.0) {
return E_MovementState.Walking;
} else {
return E_MovementState.Idle;
}
}
}
export const BFL_MovementStateMachine = new BFL_MovementStateMachineClass();

Binary file not shown.

View File

@ -1,43 +0,0 @@
// Movement/State/S_MovementContext.ts
import type { E_SurfaceType } from '#root/Movement/Surface/E_SurfaceType.ts';
import type { Float } from '#root/UE/Float.ts';
/**
* Movement context data for state determination
* Contains all information needed to determine movement state
*
* @category Movement State
*/
export interface S_MovementContext {
/**
* Whether character is on walkable ground
*/
IsGrounded: boolean;
/**
* Type of surface character is on
*/
SurfaceType: E_SurfaceType;
/**
* Magnitude of player input (0-1)
*/
InputMagnitude: Float;
/**
* Current horizontal movement speed (cm/s)
*/
CurrentSpeed: Float;
/**
* Current vertical velocity (cm/s)
* Positive = moving up, Negative = falling
*/
VerticalVelocity: Float;
/**
* Whether character is blocked by collision
*/
IsBlocked: boolean;
}

Binary file not shown.

View File

@ -1,123 +0,0 @@
// Movement/Surface/BFL_SurfaceClassifier.ts
import { BFL_Vectors } from '#root/Math/Libraries/BFL_Vectors.ts';
import { E_SurfaceType } from '#root/Movement/Surface/E_SurfaceType.ts';
import type { S_AngleThresholds } from '#root/Movement/Surface/S_AngleThresholds.ts';
import { BlueprintFunctionLibrary } from '#root/UE/BlueprintFunctionLibrary.ts';
import { Vector } from '#root/UE/Vector.ts';
class BFL_SurfaceClassifierClass extends BlueprintFunctionLibrary {
// ════════════════════════════════════════════════════════════════════════════════════════
// CLASSIFICATION
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Classify surface type based on normal vector and angle thresholds
*
* @param SurfaceNormal - Normalized surface normal vector (from hit result)
* @param AngleThresholdsRads - Angle thresholds in radians (pre-converted for performance)
* @returns Surface type classification
*
* @example
* // Flat ground (normal pointing up)
* const flat = SurfaceClassifier.Classify(new Vector(0, 0, 1), thresholds);
* // Returns: E_SurfaceType.Walkable
*
* @example
* // Steep slope (50° angle)
* const steep = SurfaceClassifier.Classify(BFL_Vectors.GetNormalFromAngle(50), thresholds);
* // Returns: E_SurfaceType.SteepSlope
*
* @pure true
* @category Classification
*/
public Classify(
SurfaceNormal: Vector,
AngleThresholdsRads: S_AngleThresholds
): E_SurfaceType {
// Calculate angle between surface normal and up vector
const surfaceAngle = BFL_Vectors.GetSurfaceAngle(SurfaceNormal);
// Classify based on angle thresholds
if (surfaceAngle <= AngleThresholdsRads.Walkable) {
return E_SurfaceType.Walkable;
} else if (surfaceAngle <= AngleThresholdsRads.SteepSlope) {
return E_SurfaceType.SteepSlope;
} else if (surfaceAngle <= AngleThresholdsRads.Wall) {
return E_SurfaceType.Wall;
} else {
return E_SurfaceType.Ceiling;
}
}
// ════════════════════════════════════════════════════════════════════════════════════════
// TYPE CHECKS
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Check if surface allows normal walking movement
*
* @param surfaceType - Surface type to check
* @returns True if surface is walkable
*
* @pure true
* @category Type Checks
*/
public IsWalkable(surfaceType: E_SurfaceType): boolean {
return surfaceType === E_SurfaceType.Walkable;
}
/**
* Check if surface causes sliding behavior
*
* @param surfaceType - Surface type to check
* @returns True if surface is steep slope
*
* @pure true
* @category Type Checks
*/
public IsSteep(surfaceType: E_SurfaceType): boolean {
return surfaceType === E_SurfaceType.SteepSlope;
}
/**
* Check if surface blocks movement (collision wall)
*
* @param surfaceType - Surface type to check
* @returns True if surface is a wall
*
* @pure true
* @category Type Checks
*/
public IsWall(surfaceType: E_SurfaceType): boolean {
return surfaceType === E_SurfaceType.Wall;
}
/**
* Check if surface is overhead (ceiling)
*
* @param surfaceType - Surface type to check
* @returns True if surface is ceiling
*
* @pure true
* @category Type Checks
*/
public IsCeiling(surfaceType: E_SurfaceType): boolean {
return surfaceType === E_SurfaceType.Ceiling;
}
/**
* Check if no surface detected (airborne state)
*
* @param surfaceType - Surface type to check
* @returns True if no surface contact
*
* @pure true
* @category Type Checks
*/
public IsNone(surfaceType: E_SurfaceType): boolean {
return surfaceType === E_SurfaceType.None;
}
}
export const BFL_SurfaceClassifier = new BFL_SurfaceClassifierClass();

Binary file not shown.

View File

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

Binary file not shown.

View File

@ -1,9 +0,0 @@
// Movement/Surface/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.

File diff suppressed because it is too large Load Diff

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 { S_ToastMessage } from '/Content/Toasts/Structs/S_ToastMessage.ts';
import type { WBP_Toast } from '#root/Toasts/UI/WBP_Toast.ts'; import type { WBP_Toast } from '/Content/Toasts/UI/WBP_Toast.ts';
import { WBP_ToastContainer } from '#root/Toasts/UI/WBP_ToastContainer.ts'; import { WBP_ToastContainer } from '/Content/Toasts/UI/WBP_ToastContainer.ts';
import { ActorComponent } from '#root/UE/ActorComponent.ts'; import { ActorComponent } from '/Content/UE/ActorComponent.ts';
import { CreateWidget } from '#root/UE/CteateWidget.ts'; import { CreateWidget } from '/Content/UE/CteateWidget.ts';
import type { Float } from '#root/UE/Float.ts'; import type { Float } from '/Content/UE/Float.ts';
import type { Integer } from '#root/UE/Integer.ts'; import type { Integer } from '/Content/UE/Integer.ts';
import { SystemLibrary } from '#root/UE/SystemLibrary.ts'; import { SystemLibrary } from '/Content/UE/SystemLibrary.ts';
import type { Text } from '#root/UE/Text.ts'; import type { Text } from '/Content/UE/Text.ts';
import { UEArray } from '#root/UE/UEArray.ts'; import { UEArray } from '/Content/UE/UEArray.ts';
import { E_MessageType } from '#root/UI/Enums/E_MessageType.ts'; import { E_MessageType } from '/Content/UI/Enums/E_MessageType.ts';
/** /**
* Toast Notification System Component * 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 { Float } from '/Content/UE/Float.ts';
import type { Integer } from '#root/UE/Integer.ts'; import type { Integer } from '/Content/UE/Integer.ts';
import type { Text } from '#root/UE/Text.ts'; import type { Text } from '/Content/UE/Text.ts';
import type { E_MessageType } from '#root/UI/Enums/E_MessageType.ts'; import type { E_MessageType } from '/Content/UI/Enums/E_MessageType.ts';
export interface S_ToastMessage { export interface S_ToastMessage {
ID: Integer; 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.

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