rewrite movement to c++
parent
9539f48a06
commit
963e7a34dc
|
|
@ -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': {
|
||||||
|
|
|
||||||
|
|
@ -93,3 +93,6 @@ ConnectionType=USBOnly
|
||||||
bUseManualIPAddress=False
|
bUseManualIPAddress=False
|
||||||
ManualIPAddress=
|
ManualIPAddress=
|
||||||
|
|
||||||
|
|
||||||
|
[CoreRedirects]
|
||||||
|
+ClassRedirects=(OldName="/Script/TengriPlatformer.UTengriCollisionResolver",NewName="/Script/TengriPlatformer.TengriCollisionResolver")
|
||||||
|
|
@ -5,3 +5,5 @@ CommonButtonAcceptKeyHandling=TriggerClick
|
||||||
|
|
||||||
[/Script/EngineSettings.GeneralProjectSettings]
|
[/Script/EngineSettings.GeneralProjectSettings]
|
||||||
ProjectID=56CEA3524FAE49EC0DF6D8A5178FEC04
|
ProjectID=56CEA3524FAE49EC0DF6D8A5178FEC04
|
||||||
|
CopyrightNotice=Request Games © All rights reserved
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
BIN
Content/Blueprints/BP_MainCharacter.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -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;
|
||||||
|
|
|
||||||
BIN
Content/Blueprints/BP_TengriGameMode.uasset (Stored with Git LFS)
BIN
Content/Blueprints/BP_TengriGameMode.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -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
|
||||||
Binary file not shown.
BIN
Content/Camera/Components/AC_Camera.uasset (Stored with Git LFS)
BIN
Content/Camera/Components/AC_Camera.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -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 должны давать одинаковые результаты на разных запусках.
|
|
||||||
|
|
@ -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 в будущих этапах разработки платформера с лучшей производительностью и безопасностью.
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
BIN
Content/Camera/Tests/FT_CameraInitialization.uasset (Stored with Git LFS)
BIN
Content/Camera/Tests/FT_CameraInitialization.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -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();
|
|
||||||
}
|
|
||||||
BIN
Content/Camera/Tests/FT_CameraLimits.uasset (Stored with Git LFS)
BIN
Content/Camera/Tests/FT_CameraLimits.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -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();
|
|
||||||
}
|
|
||||||
BIN
Content/Camera/Tests/FT_CameraRotation.uasset (Stored with Git LFS)
BIN
Content/Camera/Tests/FT_CameraRotation.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -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();
|
|
||||||
}
|
|
||||||
BIN
Content/Camera/Tests/FT_CameraSensitivity.uasset (Stored with Git LFS)
BIN
Content/Camera/Tests/FT_CameraSensitivity.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -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();
|
|
||||||
}
|
|
||||||
BIN
Content/Camera/Tests/FT_CameraSmoothing.uasset (Stored with Git LFS)
BIN
Content/Camera/Tests/FT_CameraSmoothing.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 функции работают
|
|
||||||
- [ ] Данные обновляются в реальном времени
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
BIN
Content/Debug/Tests/FT_DebugNavigation.uasset (Stored with Git LFS)
BIN
Content/Debug/Tests/FT_DebugNavigation.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -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();
|
|
||||||
}
|
|
||||||
BIN
Content/Debug/Tests/FT_DebugPageManagement.uasset (Stored with Git LFS)
BIN
Content/Debug/Tests/FT_DebugPageManagement.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -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)
BIN
Content/Debug/Tests/FT_DebugSystem.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -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)
BIN
Content/Debug/UI/WBP_DebugHUD.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -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'));
|
||||||
|
|
|
||||||
|
|
@ -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'));
|
||||||
|
|
|
||||||
|
|
@ -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'));
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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. Никаких симуляций или искусственных переключений.
|
|
||||||
|
|
@ -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 в будущих этапах разработки.
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
BIN
Content/Input/Tests/FT_InputDeviceDetection.uasset (Stored with Git LFS)
BIN
Content/Input/Tests/FT_InputDeviceDetection.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -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)
BIN
Content/Levels/TestLevel.umap (Stored with Git LFS)
Binary file not shown.
|
|
@ -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)
BIN
Content/Math/Libraries/BFL_Vectors.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -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}\n°` +
|
|
||||||
`Is Rotating: ${this.CurrentMovementState.IsRotating}\n` +
|
|
||||||
`Rotation Speed: ${this.Config.RotationSpeed}\n°` +
|
|
||||||
`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)
BIN
Content/Movement/AC_Movement.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -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();
|
|
||||||
BIN
Content/Movement/Collision/BFL_CollisionResolver.uasset (Stored with Git LFS)
BIN
Content/Movement/Collision/BFL_CollisionResolver.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -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();
|
|
||||||
BIN
Content/Movement/Collision/BFL_GroundProbe.uasset (Stored with Git LFS)
BIN
Content/Movement/Collision/BFL_GroundProbe.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -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;
|
|
||||||
}
|
|
||||||
BIN
Content/Movement/Collision/S_SweepResult.uasset (Stored with Git LFS)
BIN
Content/Movement/Collision/S_SweepResult.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -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();
|
|
||||||
BIN
Content/Movement/Core/BFL_MovementProcessor.uasset (Stored with Git LFS)
BIN
Content/Movement/Core/BFL_MovementProcessor.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -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;
|
|
||||||
}
|
|
||||||
BIN
Content/Movement/Core/DA_MovementConfig.uasset (Stored with Git LFS)
BIN
Content/Movement/Core/DA_MovementConfig.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -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;
|
|
||||||
}
|
|
||||||
BIN
Content/Movement/Core/DA_MovementConfigDefault.uasset (Stored with Git LFS)
BIN
Content/Movement/Core/DA_MovementConfigDefault.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -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',
|
|
||||||
}
|
|
||||||
BIN
Content/Movement/Core/E_MovementState.uasset (Stored with Git LFS)
BIN
Content/Movement/Core/E_MovementState.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -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;
|
|
||||||
}
|
|
||||||
BIN
Content/Movement/Core/S_MovementInput.uasset (Stored with Git LFS)
BIN
Content/Movement/Core/S_MovementInput.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -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;
|
|
||||||
}
|
|
||||||
BIN
Content/Movement/Core/S_MovementState.uasset (Stored with Git LFS)
BIN
Content/Movement/Core/S_MovementState.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -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;
|
||||||
|
}
|
||||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
|
@ -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();
|
|
||||||
BIN
Content/Movement/Physics/BFL_Kinematics.uasset (Stored with Git LFS)
BIN
Content/Movement/Physics/BFL_Kinematics.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -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();
|
|
||||||
BIN
Content/Movement/Rotation/BFL_RotationController.uasset (Stored with Git LFS)
BIN
Content/Movement/Rotation/BFL_RotationController.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -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;
|
|
||||||
}
|
|
||||||
BIN
Content/Movement/Rotation/S_RotationResult.uasset (Stored with Git LFS)
BIN
Content/Movement/Rotation/S_RotationResult.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -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();
|
|
||||||
BIN
Content/Movement/State/BFL_MovementStateMachine.uasset (Stored with Git LFS)
BIN
Content/Movement/State/BFL_MovementStateMachine.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -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;
|
|
||||||
}
|
|
||||||
BIN
Content/Movement/State/S_MovementContext.uasset (Stored with Git LFS)
BIN
Content/Movement/State/S_MovementContext.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -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();
|
|
||||||
BIN
Content/Movement/Surface/BFL_SurfaceClassifier.uasset (Stored with Git LFS)
BIN
Content/Movement/Surface/BFL_SurfaceClassifier.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -1,9 +0,0 @@
|
||||||
// Movement/Surface/E_SurfaceType.ts
|
|
||||||
|
|
||||||
export enum E_SurfaceType {
|
|
||||||
None = 'None',
|
|
||||||
Walkable = 'Walkable',
|
|
||||||
SteepSlope = 'SteepSlope',
|
|
||||||
Wall = 'Wall',
|
|
||||||
Ceiling = 'Ceiling',
|
|
||||||
}
|
|
||||||
BIN
Content/Movement/Surface/E_SurfaceType.uasset (Stored with Git LFS)
BIN
Content/Movement/Surface/E_SurfaceType.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -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;
|
|
||||||
}
|
|
||||||
BIN
Content/Movement/Surface/S_AngleThresholds.uasset (Stored with Git LFS)
BIN
Content/Movement/Surface/S_AngleThresholds.uasset (Stored with Git LFS)
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 работает при включенной настройке
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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)
BIN
Content/Toasts/Tests/FT_ToastLimit.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -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;
|
|
||||||
}
|
|
||||||
BIN
Content/Toasts/Tests/FT_ToastsDurationHandling.uasset (Stored with Git LFS)
BIN
Content/Toasts/Tests/FT_ToastsDurationHandling.uasset (Stored with Git LFS)
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue