rewrite movement to c++
parent
9539f48a06
commit
963e7a34dc
|
|
@ -43,13 +43,6 @@ module.exports = {
|
|||
'sibling',
|
||||
'index',
|
||||
],
|
||||
'pathGroups': [
|
||||
{
|
||||
'pattern': '#root/**',
|
||||
'group': 'internal',
|
||||
'position': 'before'
|
||||
}
|
||||
],
|
||||
'pathGroupsExcludedImportTypes': ['builtin'],
|
||||
'newlines-between': 'never',
|
||||
'alphabetize': {
|
||||
|
|
|
|||
|
|
@ -93,3 +93,6 @@ ConnectionType=USBOnly
|
|||
bUseManualIPAddress=False
|
||||
ManualIPAddress=
|
||||
|
||||
|
||||
[CoreRedirects]
|
||||
+ClassRedirects=(OldName="/Script/TengriPlatformer.UTengriCollisionResolver",NewName="/Script/TengriPlatformer.TengriCollisionResolver")
|
||||
|
|
@ -5,3 +5,5 @@ CommonButtonAcceptKeyHandling=TriggerClick
|
|||
|
||||
[/Script/EngineSettings.GeneralProjectSettings]
|
||||
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_DebugHUD } from '#root/Debug/Components/AC_DebugHUD.ts';
|
||||
import { AC_InputDevice } from '#root/Input/Components/AC_InputDevice.ts';
|
||||
import { IMC_Default } from '#root/Input/IMC_Default.ts';
|
||||
import { AC_Movement } from '#root/Movement/AC_Movement.ts';
|
||||
import { AC_ToastSystem } from '#root/Toasts/Components/AC_ToastSystem.ts';
|
||||
import { CapsuleComponent } from '#root/UE/CapsuleComponent.ts';
|
||||
import { Cast } from '#root/UE/Cast.ts';
|
||||
import type { Controller } from '#root/UE/Controller.ts';
|
||||
import { EnhancedInputLocalPlayerSubsystem } from '#root/UE/EnhancedInputLocalPlayerSubsystem.ts';
|
||||
import type { Float } from '#root/UE/Float.ts';
|
||||
import { MathLibrary } from '#root/UE/MathLibrary.ts';
|
||||
import { Pawn } from '#root/UE/Pawn.ts';
|
||||
import type { PlayerController } from '#root/UE/PlayerController.ts';
|
||||
import { Rotator } from '#root/UE/Rotator.ts';
|
||||
import { SystemLibrary } from '#root/UE/SystemLibrary.ts';
|
||||
import { Vector } from '#root/UE/Vector.ts';
|
||||
import { AC_Camera } from '/Content/Camera/AC_Camera.ts';
|
||||
import { AC_DebugHUD } from '/Content/Debug/Components/AC_DebugHUD.ts';
|
||||
import { AC_InputDevice } from '/Content/Input/Components/AC_InputDevice.ts';
|
||||
import { IMC_Default } from '/Content/Input/IMC_Default.ts';
|
||||
import { AC_ToastSystem } from '/Content/Toasts/Components/AC_ToastSystem.ts';
|
||||
import { CapsuleComponent } from '/Content/UE/CapsuleComponent.ts';
|
||||
import { Cast } from '/Content/UE/Cast.ts';
|
||||
import type { Controller } from '/Content/UE/Controller.ts';
|
||||
import { EnhancedInputLocalPlayerSubsystem } from '/Content/UE/EnhancedInputLocalPlayerSubsystem.ts';
|
||||
import type { Float } from '/Content/UE/Float.ts';
|
||||
import { MathLibrary } from '/Content/UE/MathLibrary.ts';
|
||||
import { Pawn } from '/Content/UE/Pawn.ts';
|
||||
import type { PlayerController } from '/Content/UE/PlayerController.ts';
|
||||
import { Rotator } from '/Content/UE/Rotator.ts';
|
||||
import { SystemLibrary } from '/Content/UE/SystemLibrary.ts';
|
||||
import { Vector } from '/Content/UE/Vector.ts';
|
||||
import { TengriMovementComponent } from '/Source/TengriPlatformer/Movement/TengriMovementComponent.ts';
|
||||
import { DA_TengriMovementConfig } from '/Content/Movement/DA_TengriMovementConfig.ts';
|
||||
|
||||
/**
|
||||
* Main Character Blueprint
|
||||
|
|
@ -123,15 +124,17 @@ export class BP_MainCharacter extends Pawn {
|
|||
return new Vector(vec1.X + vec2.X, vec1.Y + vec2.Y, 0);
|
||||
};
|
||||
|
||||
this.CurrentMovementInput = CalculateResultMovementInputVector(
|
||||
MathLibrary.GetRightVector(
|
||||
this.GetControlRotation().roll,
|
||||
0,
|
||||
this.GetControlRotation().yaw
|
||||
),
|
||||
MathLibrary.GetForwardVector(0, 0, this.GetControlRotation().yaw),
|
||||
ActionValueX,
|
||||
ActionValueY
|
||||
this.TengriMovement.SetInputVector(
|
||||
CalculateResultMovementInputVector(
|
||||
MathLibrary.GetRightVector(
|
||||
this.GetControlRotation().roll,
|
||||
0,
|
||||
this.GetControlRotation().yaw
|
||||
),
|
||||
MathLibrary.GetForwardVector(0, 0, this.GetControlRotation().yaw),
|
||||
ActionValueX,
|
||||
ActionValueY
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -139,7 +142,7 @@ export class BP_MainCharacter extends Pawn {
|
|||
* Reset movement input when move action is completed
|
||||
*/
|
||||
EnhancedInputActionMoveCompleted(): void {
|
||||
this.CurrentMovementInput = new Vector(0, 0, 0);
|
||||
this.TengriMovement.SetInputVector(new Vector(0, 0, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -163,11 +166,6 @@ export class BP_MainCharacter extends Pawn {
|
|||
);
|
||||
}
|
||||
|
||||
this.MovementComponent.InitializeMovementSystem(
|
||||
this.CharacterCapsule,
|
||||
this.DebugHUDComponent
|
||||
);
|
||||
|
||||
this.CameraComponent.InitializeCameraSystem(
|
||||
this.InputDeviceComponent,
|
||||
this.DebugHUDComponent
|
||||
|
|
@ -196,13 +194,7 @@ export class BP_MainCharacter extends Pawn {
|
|||
)
|
||||
);
|
||||
|
||||
this.MovementComponent.ProcessMovementInput(
|
||||
this.CurrentMovementInput,
|
||||
DeltaTime
|
||||
);
|
||||
|
||||
if (this.ShowDebugInfo) {
|
||||
this.MovementComponent.UpdateDebugPage();
|
||||
this.InputDeviceComponent.UpdateDebugPage();
|
||||
this.CameraComponent.UpdateDebugPage();
|
||||
}
|
||||
|
|
@ -230,6 +222,8 @@ export class BP_MainCharacter extends Pawn {
|
|||
*/
|
||||
ToastSystemComponent = new AC_ToastSystem();
|
||||
|
||||
TengriMovement = new TengriMovementComponent(DA_TengriMovementConfig);
|
||||
|
||||
/**
|
||||
* Debug HUD system - displays movement parameters and performance metrics
|
||||
* @category Components
|
||||
|
|
@ -242,12 +236,6 @@ export class BP_MainCharacter extends Pawn {
|
|||
*/
|
||||
CharacterCapsule = new CapsuleComponent();
|
||||
|
||||
/**
|
||||
* Core movement system component - handles deterministic 3D platformer movement
|
||||
* @category Components
|
||||
*/
|
||||
MovementComponent = new AC_Movement();
|
||||
|
||||
/**
|
||||
* Master debug toggle - controls all debug systems (HUD, toasts, visual debug)
|
||||
* @category Debug
|
||||
|
|
@ -259,9 +247,4 @@ export class BP_MainCharacter extends Pawn {
|
|||
* Cached delta time from last tick - used for time-based calculations
|
||||
*/
|
||||
private DeltaTime: Float = 0.0;
|
||||
|
||||
/**
|
||||
* Current movement input vector - updated by input actions
|
||||
*/
|
||||
private CurrentMovementInput: Vector = new Vector(0, 0, 0);
|
||||
}
|
||||
|
|
|
|||
BIN
Content/Blueprints/BP_MainCharacter.uasset (Stored with Git LFS)
BIN
Content/Blueprints/BP_MainCharacter.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -1,7 +1,7 @@
|
|||
// Blueprints/BP_TengriGameMode.ts
|
||||
|
||||
import { BP_MainCharacter } from '#root/Blueprints/BP_MainCharacter.ts';
|
||||
import { GameModeBase } from '#root/UE/GameModeBase.ts';
|
||||
import { BP_MainCharacter } from '/Content/Blueprints/BP_MainCharacter.ts';
|
||||
import { GameModeBase } from '/Content/UE/GameModeBase.ts';
|
||||
|
||||
export class BP_TengriGameMode extends GameModeBase {
|
||||
DefaultPawnClass = BP_MainCharacter;
|
||||
|
|
|
|||
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_InputDevice } from '#root/Input/Components/AC_InputDevice.ts';
|
||||
import { ActorComponent } from '#root/UE/ActorComponent.ts';
|
||||
import type { Float } from '#root/UE/Float.ts';
|
||||
import { MathLibrary } from '#root/UE/MathLibrary.ts';
|
||||
import { SystemLibrary } from '#root/UE/SystemLibrary.ts';
|
||||
import { Vector } from '#root/UE/Vector.ts';
|
||||
import type { AC_DebugHUD } from '/Content/Debug/Components/AC_DebugHUD.ts';
|
||||
import type { AC_InputDevice } from '/Content/Input/Components/AC_InputDevice.ts';
|
||||
import { ActorComponent } from '/Content/UE/ActorComponent.ts';
|
||||
import type { Float } from '/Content/UE/Float.ts';
|
||||
import { MathLibrary } from '/Content/UE/MathLibrary.ts';
|
||||
import { SystemLibrary } from '/Content/UE/SystemLibrary.ts';
|
||||
import { Vector } from '/Content/UE/Vector.ts';
|
||||
|
||||
/**
|
||||
* Camera System Component
|
||||
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 { WBP_DebugHUD } from '#root/Debug/UI/WBP_DebugHUD.ts';
|
||||
import type { AC_InputDevice } from '#root/Input/Components/AC_InputDevice.ts';
|
||||
import type { AC_ToastSystem } from '#root/Toasts/Components/AC_ToastSystem.ts';
|
||||
import { ActorComponent } from '#root/UE/ActorComponent.ts';
|
||||
import { CreateWidget } from '#root/UE/CteateWidget.ts';
|
||||
import { ESlateVisibility } from '#root/UE/ESlateVisibility.ts';
|
||||
import type { Float } from '#root/UE/Float.ts';
|
||||
import type { Integer } from '#root/UE/Integer.ts';
|
||||
import { SystemLibrary } from '#root/UE/SystemLibrary.ts';
|
||||
import type { Text } from '#root/UE/Text.ts';
|
||||
import { UEArray } from '#root/UE/UEArray.ts';
|
||||
import { E_MessageType } from '#root/UI/Enums/E_MessageType.ts';
|
||||
import type { S_DebugPage } from '/Content/Debug/Structs/S_DebugPage.ts';
|
||||
import { WBP_DebugHUD } from '/Content/Debug/UI/WBP_DebugHUD.ts';
|
||||
import type { AC_InputDevice } from '/Content/Input/Components/AC_InputDevice.ts';
|
||||
import type { AC_ToastSystem } from '/Content/Toasts/Components/AC_ToastSystem.ts';
|
||||
import { ActorComponent } from '/Content/UE/ActorComponent.ts';
|
||||
import { CreateWidget } from '/Content/UE/CteateWidget.ts';
|
||||
import { ESlateVisibility } from '/Content/UE/ESlateVisibility.ts';
|
||||
import type { Float } from '/Content/UE/Float.ts';
|
||||
import type { Integer } from '/Content/UE/Integer.ts';
|
||||
import { SystemLibrary } from '/Content/UE/SystemLibrary.ts';
|
||||
import type { Text } from '/Content/UE/Text.ts';
|
||||
import { UEArray } from '/Content/UE/UEArray.ts';
|
||||
import { E_MessageType } from '/Content/UI/Enums/E_MessageType.ts';
|
||||
|
||||
/**
|
||||
* Debug HUD Controller Component
|
||||
|
|
|
|||
|
|
@ -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 { Text } from '#root/UE/Text.ts';
|
||||
import type { Float } from '/Content/UE/Float.ts';
|
||||
import type { Text } from '/Content/UE/Text.ts';
|
||||
|
||||
export interface S_DebugPage {
|
||||
PageID: string;
|
||||
|
|
|
|||
|
|
@ -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 '#root/UE/SystemLibrary.ts';
|
||||
import type { Text } from '#root/UE/Text.ts';
|
||||
import { TextBlock } from '#root/UE/TextBlock.ts';
|
||||
import { UserWidget } from '#root/UE/UserWidget.ts';
|
||||
import { SystemLibrary } from '/Content/UE/SystemLibrary.ts';
|
||||
import type { Text } from '/Content/UE/Text.ts';
|
||||
import { TextBlock } from '/Content/UE/TextBlock.ts';
|
||||
import { UserWidget } from '/Content/UE/UserWidget.ts';
|
||||
|
||||
/**
|
||||
* Debug HUD Widget for displaying system information
|
||||
|
|
@ -107,13 +106,6 @@ export class WBP_DebugHUD extends UserWidget {
|
|||
// VARIABLES
|
||||
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Reference to movement component for accessing debug data
|
||||
* Set by AC_DebugHUD during initialization
|
||||
* @category Components
|
||||
*/
|
||||
public MovementComponent: AC_Movement | null = null;
|
||||
|
||||
/**
|
||||
* Current page title text
|
||||
* Updated by AC_DebugHUD when switching pages
|
||||
|
|
|
|||
BIN
Content/Debug/UI/WBP_DebugHUD.uasset (Stored with Git LFS)
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 { Name } from '#root/UE/Name.ts';
|
||||
import { InputAction } from '/Content/UE/InputAction.ts';
|
||||
import { Name } from '/Content/UE/Name.ts';
|
||||
|
||||
export const IA_LeftTrigger = new InputAction(null, new Name('IA_LeftTrigger'));
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Input/Actions/IA_Look.ts
|
||||
// Content/Input/Actions/IA_Look.ts
|
||||
|
||||
import { InputAction } from '#root/UE/InputAction.ts';
|
||||
import { Name } from '#root/UE/Name.ts';
|
||||
import { InputAction } from '/Content/UE/InputAction.ts';
|
||||
import { Name } from '/Content/UE/Name.ts';
|
||||
|
||||
export const IA_Look = new InputAction(null, new Name('IA_Look'));
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Input/Actions/IA_Move.ts
|
||||
// Content/Input/Actions/IA_Move.ts
|
||||
|
||||
import { InputAction } from '#root/UE/InputAction.ts';
|
||||
import { Name } from '#root/UE/Name.ts';
|
||||
import { InputAction } from '/Content/UE/InputAction.ts';
|
||||
import { Name } from '/Content/UE/Name.ts';
|
||||
|
||||
export const IA_Move = new InputAction(null, new Name('IA_Move'));
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Input/Actions/IA_ToggleVisualDebug.ts
|
||||
// Content/Input/Actions/IA_ToggleVisualDebug.ts
|
||||
|
||||
import { InputAction } from '#root/UE/InputAction.ts';
|
||||
import { InputAction } from '/Content/UE/InputAction.ts';
|
||||
|
||||
export const IA_ToggleVisualDebug = new InputAction(
|
||||
null,
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
// Input/Components/AC_InputDevice.ts
|
||||
// Content/Input/Components/AC_InputDevice.ts
|
||||
|
||||
import type { AC_DebugHUD } from '#root/Debug/Components/AC_DebugHUD.ts';
|
||||
import type { AC_ToastSystem } from '#root/Toasts/Components/AC_ToastSystem.ts';
|
||||
import { ActorComponent } from '#root/UE/ActorComponent.ts';
|
||||
import { EHardwareDevicePrimaryType } from '#root/UE/EHardwareDevicePrimaryType.ts';
|
||||
import type { Float } from '#root/UE/Float.ts';
|
||||
import { InputDeviceSubsystem } from '#root/UE/InputDeviceSubsystem.ts';
|
||||
import type { Integer } from '#root/UE/Integer.ts';
|
||||
import { SystemLibrary } from '#root/UE/SystemLibrary.ts';
|
||||
import { E_MessageType } from '#root/UI/Enums/E_MessageType.ts';
|
||||
import type { AC_DebugHUD } from '/Content/Debug/Components/AC_DebugHUD.ts';
|
||||
import type { AC_ToastSystem } from '/Content/Toasts/Components/AC_ToastSystem.ts';
|
||||
import { ActorComponent } from '/Content/UE/ActorComponent.ts';
|
||||
import { EHardwareDevicePrimaryType } from '/Content/UE/EHardwareDevicePrimaryType.ts';
|
||||
import type { Float } from '/Content/UE/Float.ts';
|
||||
import { InputDeviceSubsystem } from '/Content/UE/InputDeviceSubsystem.ts';
|
||||
import type { Integer } from '/Content/UE/Integer.ts';
|
||||
import { SystemLibrary } from '/Content/UE/SystemLibrary.ts';
|
||||
import { E_MessageType } from '/Content/UI/Enums/E_MessageType.ts';
|
||||
|
||||
/**
|
||||
* Input Device Detection Component
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
// Input/IMC_Default.ts
|
||||
// Content/Input/IMC_Default.ts
|
||||
|
||||
import { IA_LeftTrigger } from '#root/Input/Actions/IA_LeftTrigger.ts';
|
||||
import { IA_Look } from '#root/Input/Actions/IA_Look.ts';
|
||||
import { IA_Move } from '#root/Input/Actions/IA_Move.ts';
|
||||
import { IA_NextDebugMode } from '#root/Input/Actions/IA_NextDebugMode.ts';
|
||||
import { IA_PrevDebugMode } from '#root/Input/Actions/IA_PrevDebugMode.ts';
|
||||
import { IA_RightTrigger } from '#root/Input/Actions/IA_RightTrigger.ts';
|
||||
import { IA_ToggleHUD } from '#root/Input/Actions/IA_ToggleHUD.ts';
|
||||
import { IA_ToggleVisualDebug } from '#root/Input/Actions/IA_ToggleVisualDebug.ts';
|
||||
import { InputMappingContext } from '#root/UE/InputMappingContext.ts';
|
||||
import { Key } from '#root/UE/Key.ts';
|
||||
import { IA_LeftTrigger } from '/Content/Input/Actions/IA_LeftTrigger.ts';
|
||||
import { IA_Look } from '/Content/Input/Actions/IA_Look.ts';
|
||||
import { IA_Move } from '/Content/Input/Actions/IA_Move.ts';
|
||||
import { IA_NextDebugMode } from '/Content/Input/Actions/IA_NextDebugMode.ts';
|
||||
import { IA_PrevDebugMode } from '/Content/Input/Actions/IA_PrevDebugMode.ts';
|
||||
import { IA_RightTrigger } from '/Content/Input/Actions/IA_RightTrigger.ts';
|
||||
import { IA_ToggleHUD } from '/Content/Input/Actions/IA_ToggleHUD.ts';
|
||||
import { IA_ToggleVisualDebug } from '/Content/Input/Actions/IA_ToggleVisualDebug.ts';
|
||||
import { InputMappingContext } from '/Content/UE/InputMappingContext.ts';
|
||||
import { Key } from '/Content/UE/Key.ts';
|
||||
|
||||
export const IMC_Default = new InputMappingContext();
|
||||
|
||||
|
|
|
|||
|
|
@ -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 { 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';
|
||||
import { BP_MainCharacter } from '/Content/Blueprints/BP_MainCharacter.ts';
|
||||
|
||||
new BP_MainCharacter();
|
||||
|
||||
// Camera Tests
|
||||
const CameraInitializationTest = new FT_CameraInitialization();
|
||||
const CameraLimitsTest = new FT_CameraLimits();
|
||||
const CameraRotationTest = new FT_CameraRotation();
|
||||
const CameraSensitivityTest = new FT_CameraSensitivity();
|
||||
const CameraSmoothingTest = new FT_CameraSmoothing();
|
||||
|
||||
CameraInitializationTest.EventStartTest();
|
||||
CameraLimitsTest.EventStartTest();
|
||||
CameraRotationTest.EventStartTest();
|
||||
CameraSensitivityTest.EventStartTest();
|
||||
CameraSmoothingTest.EventStartTest();
|
||||
|
||||
// Debug Tests
|
||||
const DebugNavigationTest = new FT_DebugNavigation();
|
||||
const DebugSystemTest = new FT_DebugSystem();
|
||||
const DebugPageManagementTest = new FT_DebugPageManagement();
|
||||
|
||||
DebugNavigationTest.EventStartTest();
|
||||
DebugSystemTest.EventStartTest();
|
||||
|
||||
// Input Tests
|
||||
const InputDeviceDetectionTest = new FT_InputDeviceDetection();
|
||||
|
||||
InputDeviceDetectionTest.EventStartTest();
|
||||
|
||||
// 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 { WBP_Toast } from '#root/Toasts/UI/WBP_Toast.ts';
|
||||
import { WBP_ToastContainer } from '#root/Toasts/UI/WBP_ToastContainer.ts';
|
||||
import { ActorComponent } from '#root/UE/ActorComponent.ts';
|
||||
import { CreateWidget } from '#root/UE/CteateWidget.ts';
|
||||
import type { Float } from '#root/UE/Float.ts';
|
||||
import type { Integer } from '#root/UE/Integer.ts';
|
||||
import { SystemLibrary } from '#root/UE/SystemLibrary.ts';
|
||||
import type { Text } from '#root/UE/Text.ts';
|
||||
import { UEArray } from '#root/UE/UEArray.ts';
|
||||
import { E_MessageType } from '#root/UI/Enums/E_MessageType.ts';
|
||||
import type { S_ToastMessage } from '/Content/Toasts/Structs/S_ToastMessage.ts';
|
||||
import type { WBP_Toast } from '/Content/Toasts/UI/WBP_Toast.ts';
|
||||
import { WBP_ToastContainer } from '/Content/Toasts/UI/WBP_ToastContainer.ts';
|
||||
import { ActorComponent } from '/Content/UE/ActorComponent.ts';
|
||||
import { CreateWidget } from '/Content/UE/CteateWidget.ts';
|
||||
import type { Float } from '/Content/UE/Float.ts';
|
||||
import type { Integer } from '/Content/UE/Integer.ts';
|
||||
import { SystemLibrary } from '/Content/UE/SystemLibrary.ts';
|
||||
import type { Text } from '/Content/UE/Text.ts';
|
||||
import { UEArray } from '/Content/UE/UEArray.ts';
|
||||
import { E_MessageType } from '/Content/UI/Enums/E_MessageType.ts';
|
||||
|
||||
/**
|
||||
* Toast Notification System Component
|
||||
|
|
|
|||
|
|
@ -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 { Integer } from '#root/UE/Integer.ts';
|
||||
import type { Text } from '#root/UE/Text.ts';
|
||||
import type { E_MessageType } from '#root/UI/Enums/E_MessageType.ts';
|
||||
import type { Float } from '/Content/UE/Float.ts';
|
||||
import type { Integer } from '/Content/UE/Integer.ts';
|
||||
import type { Text } from '/Content/UE/Text.ts';
|
||||
import type { E_MessageType } from '/Content/UI/Enums/E_MessageType.ts';
|
||||
|
||||
export interface S_ToastMessage {
|
||||
ID: Integer;
|
||||
|
|
|
|||
|
|
@ -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