Compare commits
No commits in common. "8744cb759b2d378aca5de8ae71ed754fe9d1e142" and "b83388e74e0ded18c4a399c48b46bfb1885139fc" have entirely different histories.
8744cb759b
...
b83388e74e
|
|
@ -1,5 +0,0 @@
|
||||||
// Content/BasicShapes/CustomDefaultSkeletalMesh.ts
|
|
||||||
|
|
||||||
import { SkeletalMesh } from '/Content/UE/SkeletalMesh.ts';
|
|
||||||
|
|
||||||
export class CustomDefaultSkeletalMesh extends SkeletalMesh {}
|
|
||||||
BIN
Content/BasicShapes/CustomDefaultSkeletalMesh.uasset (Stored with Git LFS)
BIN
Content/BasicShapes/CustomDefaultSkeletalMesh.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -5,34 +5,26 @@ import { AC_DebugHUD } from '/Content/Debug/Components/AC_DebugHUD.ts';
|
||||||
import { AC_InputDevice } from '/Content/Input/Components/AC_InputDevice.ts';
|
import { AC_InputDevice } from '/Content/Input/Components/AC_InputDevice.ts';
|
||||||
import { IMC_Default } from '/Content/Input/IMC_Default.ts';
|
import { IMC_Default } from '/Content/Input/IMC_Default.ts';
|
||||||
import { AC_ToastSystem } from '/Content/Toasts/Components/AC_ToastSystem.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 { Cast } from '/Content/UE/Cast.ts';
|
||||||
import type { Controller } from '/Content/UE/Controller.ts';
|
import type { Controller } from '/Content/UE/Controller.ts';
|
||||||
import { EnhancedInputLocalPlayerSubsystem } from '/Content/UE/EnhancedInputLocalPlayerSubsystem.ts';
|
import { EnhancedInputLocalPlayerSubsystem } from '/Content/UE/EnhancedInputLocalPlayerSubsystem.ts';
|
||||||
import type { Float } from '/Content/UE/Float.ts';
|
import type { Float } from '/Content/UE/Float.ts';
|
||||||
import { MathLibrary } from '/Content/UE/MathLibrary.ts';
|
import { MathLibrary } from '/Content/UE/MathLibrary.ts';
|
||||||
|
import { Pawn } from '/Content/UE/Pawn.ts';
|
||||||
import type { PlayerController } from '/Content/UE/PlayerController.ts';
|
import type { PlayerController } from '/Content/UE/PlayerController.ts';
|
||||||
import { Rotator } from '/Content/UE/Rotator.ts';
|
import { Rotator } from '/Content/UE/Rotator.ts';
|
||||||
import { SystemLibrary } from '/Content/UE/SystemLibrary.ts';
|
import { SystemLibrary } from '/Content/UE/SystemLibrary.ts';
|
||||||
import { Vector } from '/Content/UE/Vector.ts';
|
import { Vector } from '/Content/UE/Vector.ts';
|
||||||
import { TengriMovementComponent } from '/Source/TengriPlatformer/Movement/TengriMovementComponent.ts';
|
import { TengriMovementComponent } from '/Source/TengriPlatformer/Movement/TengriMovementComponent.ts';
|
||||||
import { DA_TengriMovementConfig } from '/Content/Movement/DA_TengriMovementConfig.ts';
|
import { DA_TengriMovementConfig } from '/Content/Movement/DA_TengriMovementConfig.ts';
|
||||||
import { TengriCharacter } from '/Source/TengriPlatformer/Character/TengriCharacter.ts';
|
|
||||||
import { IA_Interact } from '/Content/Input/Actions/IA_Inreract.ts';
|
|
||||||
import { IA_Throw } from '/Content/Input/Actions/IA_Throw.ts';
|
|
||||||
import { IA_Aim } from '/Content/Input/Actions/IA_Aim.ts';
|
|
||||||
import { IMC_ItemHeld } from '/Content/Input/IMC_ItemHeld.ts';
|
|
||||||
import { CreateWidget } from '/Content/UE/CteateWidget.ts';
|
|
||||||
import { WBP_HUD } from '/Content/UI/WBP_HUD.ts';
|
|
||||||
import { SpringArmComponent } from '/Content/UE/SpringArmComponent.ts';
|
|
||||||
import { LinearColor } from '/Content/UE/LinearColor.ts';
|
|
||||||
import { CustomDefaultSkeletalMesh } from '/Content/BasicShapes/CustomDefaultSkeletalMesh.ts';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main Character Blueprint
|
* Main Character Blueprint
|
||||||
* Core player character with deterministic movement system
|
* Core player character with deterministic movement system
|
||||||
* Integrates debug HUD and toast notification systems
|
* Integrates debug HUD and toast notification systems
|
||||||
*/
|
*/
|
||||||
export class BP_MainCharacter extends TengriCharacter {
|
export class BP_MainCharacter extends Pawn {
|
||||||
// ════════════════════════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
// GRAPHS
|
// GRAPHS
|
||||||
// ════════════════════════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
@ -153,32 +145,6 @@ export class BP_MainCharacter extends TengriCharacter {
|
||||||
this.TengriMovement.SetInputVector(new Vector(0, 0, 0));
|
this.TengriMovement.SetInputVector(new Vector(0, 0, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
EnhancedInputActionJumpTriggered(): void {
|
|
||||||
this.MovementComponent.SetJumpInput(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
EnhancedInputActionJumpCompleted(): void {
|
|
||||||
this.MovementComponent.SetJumpInput(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
MovementComponentOnLanded(IsHeavy: boolean): void {
|
|
||||||
if (IsHeavy) {
|
|
||||||
SystemLibrary.PrintString(
|
|
||||||
'Boom! (Heavy)',
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
new LinearColor(1, 0, 0, 1)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
SystemLibrary.PrintString(
|
|
||||||
'Tap (Light)',
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
new LinearColor(0, 1, 0, 1)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize all systems when character spawns
|
* Initialize all systems when character spawns
|
||||||
* Order: Toast → Debug → Movement (movement last as it may generate debug output)
|
* Order: Toast → Debug → Movement (movement last as it may generate debug output)
|
||||||
|
|
@ -204,8 +170,6 @@ export class BP_MainCharacter extends TengriCharacter {
|
||||||
this.InputDeviceComponent,
|
this.InputDeviceComponent,
|
||||||
this.DebugHUDComponent
|
this.DebugHUDComponent
|
||||||
);
|
);
|
||||||
|
|
||||||
CreateWidget(WBP_HUD).AddToViewport();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -234,32 +198,12 @@ export class BP_MainCharacter extends TengriCharacter {
|
||||||
this.InputDeviceComponent.UpdateDebugPage();
|
this.InputDeviceComponent.UpdateDebugPage();
|
||||||
this.CameraComponent.UpdateDebugPage();
|
this.CameraComponent.UpdateDebugPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.SpringArm.TargetArmLength = MathLibrary.FInterpTo(
|
|
||||||
this.SpringArm.TargetArmLength,
|
|
||||||
this.bIsAiming ? this.AimArmLength : this.DefaultArmLength,
|
|
||||||
DeltaTime,
|
|
||||||
10.0
|
|
||||||
);
|
|
||||||
|
|
||||||
this.SpringArm.TargetOffset = MathLibrary.VInterpTo(
|
|
||||||
this.SpringArm.TargetOffset,
|
|
||||||
this.bIsAiming ? this.AimSocketOffset : this.DefaultSocketOffset,
|
|
||||||
DeltaTime,
|
|
||||||
10.0
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
// VARIABLES
|
// VARIABLES
|
||||||
// ════════════════════════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
/**
|
|
||||||
* Camera system component - handles camera rotation and sensitivity
|
|
||||||
* @category Components
|
|
||||||
*/
|
|
||||||
SpringArm = new SpringArmComponent();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Camera system component - handles camera rotation and sensitivity
|
* Camera system component - handles camera rotation and sensitivity
|
||||||
* @category Components
|
* @category Components
|
||||||
|
|
@ -278,9 +222,7 @@ export class BP_MainCharacter extends TengriCharacter {
|
||||||
*/
|
*/
|
||||||
ToastSystemComponent = new AC_ToastSystem();
|
ToastSystemComponent = new AC_ToastSystem();
|
||||||
|
|
||||||
TengriMovement = new TengriMovementComponent({
|
TengriMovement = new TengriMovementComponent(DA_TengriMovementConfig);
|
||||||
MovementConfig: DA_TengriMovementConfig,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Debug HUD system - displays movement parameters and performance metrics
|
* Debug HUD system - displays movement parameters and performance metrics
|
||||||
|
|
@ -288,6 +230,12 @@ export class BP_MainCharacter extends TengriCharacter {
|
||||||
*/
|
*/
|
||||||
DebugHUDComponent = new AC_DebugHUD();
|
DebugHUDComponent = new AC_DebugHUD();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Character's capsule component for collision detection
|
||||||
|
* @category Components
|
||||||
|
*/
|
||||||
|
CharacterCapsule = new CapsuleComponent();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Master debug toggle - controls all debug systems (HUD, toasts, visual debug)
|
* Master debug toggle - controls all debug systems (HUD, toasts, visual debug)
|
||||||
* @category Debug
|
* @category Debug
|
||||||
|
|
@ -295,43 +243,8 @@ export class BP_MainCharacter extends TengriCharacter {
|
||||||
*/
|
*/
|
||||||
private ShowDebugInfo: boolean = true;
|
private ShowDebugInfo: boolean = true;
|
||||||
|
|
||||||
/**
|
|
||||||
* @category Camera
|
|
||||||
* @instanceEditable true
|
|
||||||
*/
|
|
||||||
private readonly DefaultArmLength: Float = 400.0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @category Camera
|
|
||||||
* @instanceEditable true
|
|
||||||
*/
|
|
||||||
private readonly AimArmLength: Float = 250.0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @category Camera
|
|
||||||
* @instanceEditable true
|
|
||||||
*/
|
|
||||||
private readonly DefaultSocketOffset: Vector = new Vector(0.0, 0.0, 0.0);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @category Camera
|
|
||||||
* @instanceEditable true
|
|
||||||
*/
|
|
||||||
private readonly AimSocketOffset: Vector = new Vector(0.0, 100.0, 60.0);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cached delta time from last tick - used for time-based calculations
|
* Cached delta time from last tick - used for time-based calculations
|
||||||
*/
|
*/
|
||||||
private DeltaTime: Float = 0.0;
|
private DeltaTime: Float = 0.0;
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.InteractAction = IA_Interact;
|
|
||||||
this.ThrowAction = IA_Throw;
|
|
||||||
this.AimAction = IA_Aim;
|
|
||||||
this.ItemHeldMappingContext = IMC_ItemHeld;
|
|
||||||
|
|
||||||
this.Mesh = new CustomDefaultSkeletalMesh();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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,5 +0,0 @@
|
||||||
// Content/Blueprints/BP_Rock.ts
|
|
||||||
|
|
||||||
import { TengriPickupActor } from '/Source/TengriPlatformer/World/TengriPickupActor.ts';
|
|
||||||
|
|
||||||
export class BP_Rock extends TengriPickupActor {}
|
|
||||||
BIN
Content/Blueprints/BP_Rock.uasset (Stored with Git LFS)
BIN
Content/Blueprints/BP_Rock.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -1,6 +0,0 @@
|
||||||
// Content/Input/Actions/IA_Aim
|
|
||||||
|
|
||||||
import { InputAction } from '/Content/UE/InputAction.ts';
|
|
||||||
import { Name } from '/Content/UE/Name.ts';
|
|
||||||
|
|
||||||
export const IA_Aim = new InputAction(null, new Name('IA_Aim'));
|
|
||||||
BIN
Content/Input/Actions/IA_Aim.uasset (Stored with Git LFS)
BIN
Content/Input/Actions/IA_Aim.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -1,6 +0,0 @@
|
||||||
// Content/Input/Actions/IA_Interact
|
|
||||||
|
|
||||||
import { InputAction } from '/Content/UE/InputAction.ts';
|
|
||||||
import { Name } from '/Content/UE/Name.ts';
|
|
||||||
|
|
||||||
export const IA_Interact = new InputAction(null, new Name('IA_Interact'));
|
|
||||||
BIN
Content/Input/Actions/IA_Interact.uasset (Stored with Git LFS)
BIN
Content/Input/Actions/IA_Interact.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -1,6 +0,0 @@
|
||||||
// Content/Input/Actions/IA_Jump
|
|
||||||
|
|
||||||
import { InputAction } from '/Content/UE/InputAction.ts';
|
|
||||||
import { Name } from '/Content/UE/Name.ts';
|
|
||||||
|
|
||||||
export const IA_Jump = new InputAction(null, new Name('IA_Jump'));
|
|
||||||
BIN
Content/Input/Actions/IA_Jump.uasset (Stored with Git LFS)
BIN
Content/Input/Actions/IA_Jump.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -1,5 +0,0 @@
|
||||||
// Content/Input/Actions/IA_Aim
|
|
||||||
|
|
||||||
import { InputAction } from '/Content/UE/InputAction.ts';
|
|
||||||
import { Name } from '/Content/UE/Name.ts';
|
|
||||||
export const IA_Throw = new InputAction(null, new Name('IA_Aim'));
|
|
||||||
BIN
Content/Input/Actions/IA_Throw.uasset (Stored with Git LFS)
BIN
Content/Input/Actions/IA_Throw.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -10,8 +10,6 @@ import { IA_ToggleHUD } from '/Content/Input/Actions/IA_ToggleHUD.ts';
|
||||||
import { IA_ToggleVisualDebug } from '/Content/Input/Actions/IA_ToggleVisualDebug.ts';
|
import { IA_ToggleVisualDebug } from '/Content/Input/Actions/IA_ToggleVisualDebug.ts';
|
||||||
import { InputMappingContext } from '/Content/UE/InputMappingContext.ts';
|
import { InputMappingContext } from '/Content/UE/InputMappingContext.ts';
|
||||||
import { Key } from '/Content/UE/Key.ts';
|
import { Key } from '/Content/UE/Key.ts';
|
||||||
import { IA_Jump } from '/Content/Input/Actions/IA_Jump.ts';
|
|
||||||
import { IA_Interact } from '/Content/Input/Actions/IA_Inreract.ts';
|
|
||||||
|
|
||||||
export const IMC_Default = new InputMappingContext();
|
export const IMC_Default = new InputMappingContext();
|
||||||
|
|
||||||
|
|
@ -23,5 +21,3 @@ IMC_Default.mapKey(IA_ToggleHUD, new Key('IA_ToggleHUD'));
|
||||||
IMC_Default.mapKey(IA_ToggleVisualDebug, new Key('IA_ToggleVisualDebug'));
|
IMC_Default.mapKey(IA_ToggleVisualDebug, new Key('IA_ToggleVisualDebug'));
|
||||||
IMC_Default.mapKey(IA_Look, new Key('IA_Look'));
|
IMC_Default.mapKey(IA_Look, new Key('IA_Look'));
|
||||||
IMC_Default.mapKey(IA_Move, new Key('IA_Move'));
|
IMC_Default.mapKey(IA_Move, new Key('IA_Move'));
|
||||||
IMC_Default.mapKey(IA_Jump, new Key('IA_Jump'));
|
|
||||||
IMC_Default.mapKey(IA_Interact, new Key('IA_Interact'));
|
|
||||||
|
|
|
||||||
BIN
Content/Input/IMC_Default.uasset (Stored with Git LFS)
BIN
Content/Input/IMC_Default.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -1,11 +0,0 @@
|
||||||
// Content/Input/IMC_ItemHeld.ts
|
|
||||||
|
|
||||||
import { InputMappingContext } from '/Content/UE/InputMappingContext.ts';
|
|
||||||
import { Key } from '/Content/UE/Key.ts';
|
|
||||||
import { IA_Aim } from '/Content/Input/Actions/IA_Aim.ts';
|
|
||||||
import { IA_Throw } from '/Content/Input/Actions/IA_Throw.ts';
|
|
||||||
|
|
||||||
export const IMC_ItemHeld = new InputMappingContext();
|
|
||||||
|
|
||||||
IMC_ItemHeld.mapKey(IA_Aim, new Key('IA_Aim'));
|
|
||||||
IMC_ItemHeld.mapKey(IA_Throw, new Key('IA_Throw'));
|
|
||||||
BIN
Content/Input/IMC_ItemHeld.uasset (Stored with Git LFS)
BIN
Content/Input/IMC_ItemHeld.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -1,7 +1,5 @@
|
||||||
// Content/Levels/TestLevel.ts
|
// Content/Levels/TestLevel.ts
|
||||||
|
|
||||||
import { BP_MainCharacter } from '/Content/Blueprints/BP_MainCharacter.ts';
|
import { BP_MainCharacter } from '/Content/Blueprints/BP_MainCharacter.ts';
|
||||||
import { BP_Rock } from '/Content/Blueprints/BP_Rock.ts';
|
|
||||||
|
|
||||||
new BP_MainCharacter();
|
new BP_MainCharacter();
|
||||||
new BP_Rock();
|
|
||||||
|
|
|
||||||
BIN
Content/Levels/TestLevel.umap (Stored with Git LFS)
BIN
Content/Levels/TestLevel.umap (Stored with Git LFS)
Binary file not shown.
BIN
Content/Movement/DA_TengriMovementConfig.uasset (Stored with Git LFS)
BIN
Content/Movement/DA_TengriMovementConfig.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -1,10 +0,0 @@
|
||||||
// Content/UE/ArrowComponent.ts
|
|
||||||
|
|
||||||
import { Name } from '/Content/UE/Name.ts';
|
|
||||||
import { UEObject } from '/Content/UE/UEObject.ts';
|
|
||||||
|
|
||||||
export class ArrowComponent extends UEObject {
|
|
||||||
constructor(outer: UEObject | null = null, name: Name | string = Name.NONE) {
|
|
||||||
super(outer, name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -114,50 +114,6 @@ class MathLibraryClass extends BlueprintFunctionLibrary {
|
||||||
return Current + DeltaMove;
|
return Current + DeltaMove;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Interpolate a vector value towards a target
|
|
||||||
* @param Current - Current value
|
|
||||||
* @param Target - Target value
|
|
||||||
* @param DeltaTime - Time since last update
|
|
||||||
* @param InterpSpeed - Speed of interpolation
|
|
||||||
* @returns New interpolated value
|
|
||||||
* @example
|
|
||||||
* // Interpolate Vector(0,0,0) towards Vector(10,10,0) over 1 second at speed 5
|
|
||||||
* VInterpTo(new Vector(0,0,0), new Vector(10,10,0), 1, 5) // returns Vector(5,5,0)
|
|
||||||
*/
|
|
||||||
public VInterpTo(
|
|
||||||
Current: Vector,
|
|
||||||
Target: Vector,
|
|
||||||
DeltaTime: Float,
|
|
||||||
InterpSpeed: Float
|
|
||||||
): Vector {
|
|
||||||
if (InterpSpeed <= 0) {
|
|
||||||
return Target;
|
|
||||||
}
|
|
||||||
const Dist = new Vector(
|
|
||||||
Target.X - Current.X,
|
|
||||||
Target.Y - Current.Y,
|
|
||||||
Target.Z - Current.Z
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
this.VectorLength(
|
|
||||||
new Vector(Dist.X * Dist.X, Dist.Y * Dist.Y, Dist.Z * Dist.Z)
|
|
||||||
) < 0.00001
|
|
||||||
) {
|
|
||||||
return Target;
|
|
||||||
}
|
|
||||||
const DeltaMove = new Vector(
|
|
||||||
Dist.X * Math.min(DeltaTime * InterpSpeed, 1),
|
|
||||||
Dist.Y * Math.min(DeltaTime * InterpSpeed, 1),
|
|
||||||
Dist.Z * Math.min(DeltaTime * InterpSpeed, 1)
|
|
||||||
);
|
|
||||||
return new Vector(
|
|
||||||
Current.X + DeltaMove.X,
|
|
||||||
Current.Y + DeltaMove.Y,
|
|
||||||
Current.Z + DeltaMove.Z
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get right vector from roll, pitch, yaw angles
|
* Get right vector from roll, pitch, yaw angles
|
||||||
* @param Roll - Rotation around forward axis in radians
|
* @param Roll - Rotation around forward axis in radians
|
||||||
|
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
// Content/UE/SkeletalMesh.ts
|
|
||||||
|
|
||||||
import { SkinnedAsset } from '/Content/UE/SkinnedAsset.ts';
|
|
||||||
import { UEObject } from '/Content/UE/UEObject.ts';
|
|
||||||
import { Name } from '/Content/UE/Name.ts';
|
|
||||||
|
|
||||||
export class SkeletalMesh extends SkinnedAsset {
|
|
||||||
constructor(outer: UEObject | null = null, name: Name | string = Name.NONE) {
|
|
||||||
super(outer, name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
// Content/UE/SkinnedAsset.ts
|
|
||||||
|
|
||||||
import { StreamableRenderAsset } from '/Content/UE/StreamableRenderAsset.ts';
|
|
||||||
import { UEObject } from '/Content/UE/UEObject.ts';
|
|
||||||
import { Name } from '/Content/UE/Name.ts';
|
|
||||||
|
|
||||||
export class SkinnedAsset extends StreamableRenderAsset {
|
|
||||||
constructor(outer: UEObject | null = null, name: Name | string = Name.NONE) {
|
|
||||||
super(outer, name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
// Content/UE/SpringArmComponent.ts
|
|
||||||
|
|
||||||
import { SceneComponent } from '/Content/UE/SceneComponent.ts';
|
|
||||||
import type { UEObject } from '/Content/UE/UEObject.ts';
|
|
||||||
import type { Float } from '/Content/UE/Float.ts';
|
|
||||||
import { Vector } from '/Content/UE/Vector.ts';
|
|
||||||
|
|
||||||
export class SpringArmComponent extends SceneComponent {
|
|
||||||
constructor(outer: UEObject | null = null, name: string = 'None') {
|
|
||||||
super(outer, name);
|
|
||||||
|
|
||||||
this.TargetArmLength = 0.0;
|
|
||||||
this.TargetOffset = new Vector(0.0, 0.0, 0.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public TargetArmLength: Float;
|
|
||||||
public TargetOffset: Vector;
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
// Content/UE/StreamableRenderAsset.ts
|
|
||||||
|
|
||||||
import { Name } from '/Content/UE/Name.ts';
|
|
||||||
import { UEObject } from '/Content/UE/UEObject.ts';
|
|
||||||
|
|
||||||
export class StreamableRenderAsset extends UEObject {
|
|
||||||
constructor(outer: UEObject | null = null, name: Name | string = Name.NONE) {
|
|
||||||
super(outer, name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
// Content/UI/WBP_HUD.ts
|
|
||||||
|
|
||||||
import { UserWidget } from '/Content/UE/UserWidget.ts';
|
|
||||||
import { BP_MainCharacter } from '/Content/Blueprints/BP_MainCharacter.ts';
|
|
||||||
import { ESlateVisibility } from '/Content/UE/ESlateVisibility.ts';
|
|
||||||
|
|
||||||
export class WBP_HUD extends UserWidget {
|
|
||||||
private ChangeVisibility(): ESlateVisibility {
|
|
||||||
const player = new BP_MainCharacter();
|
|
||||||
|
|
||||||
return player.bIsAiming
|
|
||||||
? ESlateVisibility.Visible
|
|
||||||
: ESlateVisibility.Hidden;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BIN
Content/UI/WBP_HUD.uasset (Stored with Git LFS)
BIN
Content/UI/WBP_HUD.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -1,476 +1,553 @@
|
||||||
# TengriPlatformer — Roadmap v2.0
|
[//]: # (Documentation/Roadmap.md)
|
||||||
|
|
||||||
> **Статус:** Этапы 1-12 завершены
|
# Этап 1: Базовая настройка и константы
|
||||||
> **Фокус:** Прототип уровня "Подвал" + полноценная система движения
|
**Цель:** Система классификации поверхностей по углам
|
||||||
> **Принцип:** Сначала базовое движение, потом механики уровня, потом полировка
|
|
||||||
|
**Результат:** Стабильная база для всех последующих расчетов
|
||||||
|
|
||||||
|
**Что реализуем:**
|
||||||
|
- Переменные движения (MaxSpeed, Acceleration, Friction, Gravity)
|
||||||
|
- Система углов поверхностей (Walkable ≤50°, SteepSlope ≤85°, Wall ≤95°, Ceiling >95°)
|
||||||
|
- Конвертация градусы ↔ радианы
|
||||||
|
- Функции инициализации и тестирования констант
|
||||||
|
- Enhanced Input System интеграция
|
||||||
|
|
||||||
|
**Критерии успеха:**
|
||||||
|
- ✅ Корректная конвертация углов (точность <0.001)
|
||||||
|
- ✅ Все константы инициализируются при старте
|
||||||
|
- ✅ Debug вывод показывает правильные значения
|
||||||
|
- ✅ Автоматические тесты проходят
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ✅ ЗАВЕРШЁННЫЕ ЭТАПЫ (1-12)
|
# Этап 2: Debug HUD система
|
||||||
|
**Цель:** Статичный debug вывод для удобной отладки
|
||||||
|
|
||||||
| # | Название | Статус |
|
**Результат:** Профессиональная debug система
|
||||||
|---|----------|--------|
|
|
||||||
| 1 | Инициализация проекта | ✅ |
|
**Что реализуем:**
|
||||||
| 2 | Debug HUD система | ✅ |
|
- Цветовое кодирование разных типов информации
|
||||||
| 3 | Toast уведомления | ✅ |
|
- Функции переключения debug режимов
|
||||||
| 4 | Камера система | ✅ |
|
- Контроль частоты обновления HUD
|
||||||
| 5 | Детекция устройства ввода | ✅ |
|
|
||||||
| 6 | Enhanced Input настройка | ✅ |
|
**Критерии успеха:**
|
||||||
| 7 | Базовое движение по земле | ✅ |
|
- ✅ Информация отображается статично на экране
|
||||||
| 8 | Поворот персонажа | ✅ |
|
- ✅ Цветовая дифференциация работает
|
||||||
| 9 | Sweep collision | ✅ |
|
- ✅ Легкое включение/выключение debug режимов
|
||||||
| 10 | Стены и углы (wall sliding, step-up) | ✅ |
|
- ✅ Нет влияния на производительность
|
||||||
| 11 | Ground snapping и склоны | ✅ |
|
|
||||||
| 12 | Fixed Timestep + Interpolation | ✅ |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# 🔴 ФАЗА 1: CORE MOVEMENT (Критический путь)
|
# Этап 3: Система сообщений в виде тостов
|
||||||
> Без этого нельзя тестировать ничего другое
|
**Цель:** Удобный вывод сообщений для отладки
|
||||||
|
|
||||||
|
**Результат:** Система сообщений в виде тостов
|
||||||
|
|
||||||
|
**Что реализуем:**
|
||||||
|
- Функции для отображения сообщений в виде тостов
|
||||||
|
- Цветовая дифференциация сообщений
|
||||||
|
- Контроль времени отображения тостов
|
||||||
|
- Подстройка положения тостов на экране в зависимости от их количества
|
||||||
|
- Анимация появления и исчезновения тостов
|
||||||
|
|
||||||
|
**Критерии успеха:**
|
||||||
|
- ✅ Сообщения отображаются в виде тостов
|
||||||
|
- ✅ Цветовая дифференциация работает
|
||||||
|
- ✅ Тосты исчезают через заданное время
|
||||||
|
- ✅ Положение тостов адаптируется в зависимости от их количества
|
||||||
|
- ✅ Анимация появления и исчезновения тостов плавная и не вызывает рывков
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Этап 13: Система прыжков
|
# Этап 4: Детекция поверхностей
|
||||||
**Цель:** Отзывчивое управление уровня лучших платформеров
|
**Цель:** Надежное определение типа поверхности под персонажем
|
||||||
**Блокирует:** ВСЕ механики уровня требуют прыжков
|
|
||||||
|
|
||||||
**Реализация:**
|
**Результат:** Стабильная классификация Walkable/SteepSlope/Wall/Ceiling
|
||||||
|
|
||||||
|
**Что реализуем:**
|
||||||
|
- Функции классификации поверхности по нормали
|
||||||
|
- Функции запросов состояния (IsSurfaceWalkable, IsSurfaceSteep, etc.)
|
||||||
|
- Вывод необходимых значений в Debug HUD
|
||||||
|
- Вывод результатов тестов в HUD
|
||||||
|
|
||||||
|
**Критерии успеха:**
|
||||||
|
- ✅ Точная классификация поверхностей по углам
|
||||||
|
- ✅ Стабильное определение типа поверхности
|
||||||
|
- ✅ Корректная работа с нормалями поверхностей
|
||||||
|
- ✅ Детальная debug информация
|
||||||
|
- ✅ Значения корректно отображаются в Debug HUD
|
||||||
|
- ✅ Результаты тестов отображаются в HUD
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Этап 5: Детекция текущего игрового девайса
|
||||||
|
**Цель:** Определение типа устройства ввода (мышь/клавиатура)
|
||||||
|
|
||||||
|
**Результат:** Стабильное определение типа устройства ввода
|
||||||
|
|
||||||
|
**Что реализуем:**
|
||||||
|
- Функции определения типа устройства (E_InputDeviceType)
|
||||||
|
- Функции проверки состояния устройства (IsKeyboard, IsGamepad)
|
||||||
|
- Смена подсказок в HUD в зависимости от устройства
|
||||||
|
- Вывод необходимых значений в Debug HUD
|
||||||
|
- Вывод результатов тестов в HUD
|
||||||
|
|
||||||
|
**Критерии успеха:**
|
||||||
|
- ✅ Корректное определение типа устройства ввода
|
||||||
|
- ✅ Подсказки в HUD меняются в зависимости от устройства
|
||||||
|
- ✅ Легкая интеграция с Enhanced Input System
|
||||||
|
- ✅ Отсутствие ошибок при смене устройства
|
||||||
|
- ✅ Значения корректно отображаются в Debug HUD
|
||||||
|
- ✅ Результаты тестов отображаются в HUD
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Этап 6: Вращение камерой мышкой или стиком
|
||||||
|
**Цель:** Плавное вращение камеры с учетом устройства ввода
|
||||||
|
|
||||||
|
**Результат:** Плавное управление камерой
|
||||||
|
|
||||||
|
**Что реализуем:**
|
||||||
|
- Плавное вращение камеры при движении мышью или стиком геймпада
|
||||||
|
- Учет чувствительности и инверсии осей
|
||||||
|
- Вывод необходимых значений в Debug HUD
|
||||||
|
- Вывод результатов тестов в HUD
|
||||||
|
|
||||||
|
**Критерии успеха:**
|
||||||
|
- ✅ Плавное вращение камеры при движении мышью
|
||||||
|
- ✅ Плавное вращение камеры при движении стиком геймпада
|
||||||
|
- ✅ Учет чувствительности и инверсии осей
|
||||||
|
- ✅ Отсутствие рывков и заиканий
|
||||||
|
- ✅ Значения корректно отображаются в Debug HUD
|
||||||
|
- ✅ Результаты тестов отображаются в HUD
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Этап 7: Базовое движение по земле
|
||||||
|
**Цель:** Плавное детерминированное движение по плоским поверхностям
|
||||||
|
|
||||||
|
**Результат:** Отзывчивое управление без рывков и заиканий
|
||||||
|
|
||||||
|
**Что реализуем:**
|
||||||
|
- VInterpTo для плавного ускорения и торможения
|
||||||
|
- Применение гравитации с правильным обнулением на земле
|
||||||
|
- Горизонтальное движение только на walkable поверхностях
|
||||||
|
- Ограничение максимальной скорости
|
||||||
|
- Вывод необходимых значений в Debug HUD
|
||||||
|
- Вывод результатов тестов в HUD
|
||||||
|
|
||||||
|
**Критерии успеха:**
|
||||||
|
- ✅ Плавное ускорение при нажатии WASD и стика геймпада
|
||||||
|
- ✅ Плавное торможение при отпускании клавиш/стика геймпада
|
||||||
|
- ✅ Скорость не превышает MaxSpeed
|
||||||
|
- ✅ Диагональное движение не быстрее прямого
|
||||||
|
- ✅ Стабильное поведение на земле
|
||||||
|
- ✅ Значения корректно отображаются в Debug HUD
|
||||||
|
- ✅ Результаты тестов отображаются в HUD
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Этап 8: Поворот персонажа вслед за движением
|
||||||
|
**Цель:** Плавный поворот персонажа в сторону движения
|
||||||
|
|
||||||
|
**Результат:** Персонаж естественно реагирует на направление движения
|
||||||
|
|
||||||
|
**Что реализуем:**
|
||||||
|
- При использовании мыши или стика геймпада персонаж поворачивается в сторону движения
|
||||||
|
- Учет наклона камеры для корректного поворота
|
||||||
|
- Вывод необходимых значений в Debug HUD
|
||||||
|
- Вывод результатов тестов в HUD
|
||||||
|
|
||||||
|
**Критерии успеха:**
|
||||||
|
- ✅ Персонаж плавно поворачивается в сторону движения
|
||||||
|
- ✅ Поворот учитывает наклон камеры
|
||||||
|
- ✅ Плавный переход между направлениями
|
||||||
|
- ✅ Нет рывков при повороте
|
||||||
|
- ✅ Персонаж не поворачивается, если не движется
|
||||||
|
- ✅ Поворот не влияет на скорость движения
|
||||||
|
- ✅ Значения корректно отображаются в Debug HUD
|
||||||
|
- ✅ Результаты тестов отображаются в HUD
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Этап 9: Детерминированный Sweep collision
|
||||||
|
**Цель:** Полное устранение tunneling через stepped collision detection
|
||||||
|
**Результат:** Bullet-proof система коллизий
|
||||||
|
|
||||||
|
**Что реализуем:**
|
||||||
|
- PerformDeterministicSweep с пошаговой проверкой
|
||||||
|
- HandleSweepCollision для обработки ударов
|
||||||
|
- Адаптивный размер шагов sweep
|
||||||
|
- Вывод необходимых значений в Debug HUD
|
||||||
|
- Вывод результатов тестов в HUD
|
||||||
|
|
||||||
|
**Критерии успеха:**
|
||||||
|
- ✅ Полное отсутствие tunneling при любых скоростях
|
||||||
|
- ✅ Стабильная Z позиция (разброс <0.5 единиц)
|
||||||
|
- ✅ Детерминированность (100% воспроизводимость)
|
||||||
|
- ✅ Performance <25 collision checks за кадр
|
||||||
|
- ✅ Значения корректно отображаются в Debug HUD
|
||||||
|
- ✅ Результаты тестов отображаются в HUD
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Этап 10: Обработка стен и углов
|
||||||
|
**Цель:** Плавное скольжение вдоль стен без застреваний
|
||||||
|
**Результат:** Качественная навигация в сложной геометрии
|
||||||
|
|
||||||
|
**Что реализуем:**
|
||||||
|
- Wall sliding - скольжение вдоль стен
|
||||||
|
- Corner resolution - обработка внутренних углов
|
||||||
|
- Multi-directional sweep - несколько попыток движения
|
||||||
|
- Edge detection и step-up механика
|
||||||
|
- Вывод необходимых значений в Debug HUD
|
||||||
|
- Вывод результатов тестов в HUD
|
||||||
|
|
||||||
|
**Критерии успеха:**
|
||||||
|
- ✅ Персонаж не застревает в углах
|
||||||
|
- ✅ Плавное скольжение вдоль стен любой геометрии
|
||||||
|
- ✅ Автоматический step-up на небольшие препятствия
|
||||||
|
- ✅ Работает в сложных лабиринтах
|
||||||
|
- ✅ Значения корректно отображаются в Debug HUD
|
||||||
|
- ✅ Результаты тестов отображаются в HUD
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Этап 11: Движение по склонам
|
||||||
|
**Цель:** Реалистичное поведение на наклонных поверхностях
|
||||||
|
**Результат:** Естественное движение по пандусам и скатывание
|
||||||
|
|
||||||
|
**Что реализуем:**
|
||||||
|
- Slope walking - движение вверх/вниз по склонам ≤45°
|
||||||
|
- Slope sliding - скатывание с крутых поверхностей >45°
|
||||||
|
- Ground snapping - прилипание к неровной поверхности
|
||||||
|
- Momentum preservation на склонах
|
||||||
|
- Вывод необходимых значений в Debug HUD
|
||||||
|
- Вывод результатов тестов в HUD
|
||||||
|
|
||||||
|
**Критерии успеха:**
|
||||||
|
- ✅ Плавный подъем по пандусам ≤45°
|
||||||
|
- ✅ Реалистичное скатывание с крутых склонов >45°
|
||||||
|
- ✅ Отсутствие "прыжков" на неровностях
|
||||||
|
- ✅ Сохранение импульса при переходах между поверхностями
|
||||||
|
- ✅ Значения корректно отображаются в Debug HUD
|
||||||
|
- ✅ Результаты тестов отображаются в HUD
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Этап 12: Разделение физики и рендера
|
||||||
|
**Цель:** Детерминированная физика + плавная визуализация
|
||||||
|
**Результат:** AAA-качество визуального движения
|
||||||
|
|
||||||
|
**Что реализуем:**
|
||||||
|
- Dual position system (physics + render positions)
|
||||||
|
- Position interpolation для плавности
|
||||||
|
- Fixed timestep для физики (120Hz physics, variable render)
|
||||||
|
- Smooth transitions между состояниями
|
||||||
|
- Вывод необходимых значений в Debug HUD
|
||||||
|
- Вывод результатов тестов в HUD
|
||||||
|
|
||||||
|
**Критерии успеха:**
|
||||||
|
- ✅ Физика остается детерминированной
|
||||||
|
- ✅ Визуально плавное движение без микрозаиканий
|
||||||
|
- ✅ Stable 60+ FPS без влияния на физику
|
||||||
|
- ✅ Smooth interpolation работает корректно
|
||||||
|
- ✅ Значения корректно отображаются в Debug HUD
|
||||||
|
- ✅ Результаты тестов отображаются в HUD
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Этап 13: Профессиональная камера система
|
||||||
|
**Цель:** Плавная камера уровня AAA-игр
|
||||||
|
**Результат:** Комфортная камера без рывков
|
||||||
|
|
||||||
|
**Что реализуем:**
|
||||||
|
- Camera lag и damping для плавного следования
|
||||||
|
- Look-ahead prediction (камера смотрит вперед при движении)
|
||||||
|
- Smooth rotation следования за поворотами
|
||||||
|
- Dead zone для микродвижений
|
||||||
|
- Collision avoidance для камеры
|
||||||
|
- Вывод необходимых значений в Debug HUD
|
||||||
|
- Вывод результатов тестов в HUD
|
||||||
|
|
||||||
|
**Критерии успеха:**
|
||||||
|
- ✅ Камера не дергается при остановке/старте
|
||||||
|
- ✅ Плавные повороты и наклоны
|
||||||
|
- ✅ Предсказание направления движения
|
||||||
|
- ✅ Нет проваливания камеры в стены
|
||||||
|
- ✅ Значения корректно отображаются в Debug HUD
|
||||||
|
- ✅ Результаты тестов отображаются в HUD
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Этап 14: Adaptive stepping optimization
|
||||||
|
**Цель:** Оптимизация производительности sweep системы
|
||||||
|
**Результат:** Меньше collision checks без потери качества
|
||||||
|
|
||||||
|
**Что реализуем:**
|
||||||
|
- Variable step size в зависимости от скорости
|
||||||
|
- Субпиксельная точность для медленного движения
|
||||||
|
- Performance monitoring и auto-tuning
|
||||||
|
- Spatial optimization для collision queries
|
||||||
|
- Вывод необходимых значений в Debug HUD
|
||||||
|
- Вывод результатов тестов в HUD
|
||||||
|
|
||||||
|
**Критерии успеха:**
|
||||||
|
- ✅ <10 collision checks при обычном движении
|
||||||
|
- ✅ Субпиксельная точность при медленном движении
|
||||||
|
- ✅ Автоматическая адаптация под нагрузку
|
||||||
|
- ✅ Stable performance в сложных сценах
|
||||||
|
- ✅ Значения корректно отображаются в Debug HUD
|
||||||
|
- ✅ Результаты тестов отображаются в HUD
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Этап 15: Enhanced ground snapping
|
||||||
|
**Цель:** Плавное прилипание к неровным поверхностям
|
||||||
|
**Результат:** Персонаж идет по неровной геометрии без отрыва
|
||||||
|
|
||||||
|
**Что реализуем:**
|
||||||
|
- Multi-point ground detection
|
||||||
|
- Intelligent surface normal blending
|
||||||
|
- Smooth height transitions
|
||||||
|
- Predictive ground snapping
|
||||||
|
- Вывод необходимых значений в Debug HUD
|
||||||
|
- Вывод результатов тестов в HUD
|
||||||
|
|
||||||
|
**Критерии успеха:**
|
||||||
|
- ✅ Плавное движение по ступенькам
|
||||||
|
- ✅ Нет отрыва от неровной поверхности
|
||||||
|
- ✅ Smooth transitions на изменениях высоты
|
||||||
|
- ✅ Работает на любой сложности геометрии
|
||||||
|
- ✅ Значения корректно отображаются в Debug HUD
|
||||||
|
- ✅ Результаты тестов отображаются в HUD
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Этап 16: Система прыжков
|
||||||
|
**Цель:** Отзывчивое воздушное управление уровня лучших платформеров
|
||||||
|
**Результат:** Качественный платформинг с точным контролем
|
||||||
|
|
||||||
|
**Что реализуем:**
|
||||||
- Variable jump height (короткое/длинное нажатие)
|
- Variable jump height (короткое/длинное нажатие)
|
||||||
- Air control с ограничениями и инерцией
|
- Air control с ограничениями и инерцией
|
||||||
- Coyote time (прыжок после покидания платформы, ~100-150ms)
|
- Coyote time (прыжок после покидания платформы)
|
||||||
- Jump buffering (ранние нажатия прыжка)
|
- Jump buffering (ранние нажатия прыжка)
|
||||||
- Landing detection и recovery
|
- Multi-jump система (опционально)
|
||||||
|
- Вывод необходимых значений в Debug HUD
|
||||||
|
- Вывод результатов тестов в HUD
|
||||||
|
|
||||||
**Критерии:**
|
**Критерии успеха:**
|
||||||
- [ ] Точный контроль высоты прыжка
|
- ✅ Точный контроль высоты прыжка
|
||||||
- [ ] Forgiving timing (coyote + buffer)
|
- ✅ Forgiving jump timing (coyote + buffer)
|
||||||
- [ ] Responsive но не overpowered air control
|
- ✅ Responsive но не overpowered air control
|
||||||
- [ ] Плавные transitions ground ↔ air
|
- ✅ Плавные transitions между ground/air состояниями
|
||||||
|
- ✅ Значения корректно отображаются в Debug HUD
|
||||||
**Время:** ~4-6 часов
|
- ✅ Результаты тестов отображаются в HUD
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Этап 14: Воздушная физика
|
# Этап 17: Custom Camera Collision System
|
||||||
**Цель:** Естественное поведение в воздухе
|
**Цель:** Детерминированная замена SpringArm collision detection
|
||||||
**Нужно для:** Качественный platforming feel
|
**Результат:** Полный контроль над camera collision behavior
|
||||||
|
|
||||||
**Реализация:**
|
**Что реализуем:**
|
||||||
- Gravity curve (быстрее падение чем подъём)
|
- Custom sphere trace от character к target camera position
|
||||||
- Terminal velocity
|
- Smooth camera pull-in при collision с obstacles
|
||||||
- Air resistance (опционально)
|
- Character highlighting когда он за препятствием (Mario Odyssey style)
|
||||||
|
- Safe camera position recovery при застревании в геометрии
|
||||||
|
- Debug visualization для camera collision traces
|
||||||
|
- Integration с visual debug system
|
||||||
|
|
||||||
|
**Критерии успеха:**
|
||||||
|
- ✅ Камера никогда не проваливается сквозь препятствия
|
||||||
|
- ✅ Плавное приближение камеры при collision
|
||||||
|
- ✅ Character остается видимым (outline/highlight) когда за препятствием
|
||||||
|
- ✅ Детерминированное поведение на всех платформах
|
||||||
|
- ✅ Debug traces показывают camera collision queries
|
||||||
|
- ✅ Performance impact <0.1ms per frame
|
||||||
|
|
||||||
|
# Этап 18: Воздушная физика
|
||||||
|
**Цель:** Реалистичная но игровая воздушная физика
|
||||||
|
**Результат:** Естественное поведение в полете
|
||||||
|
|
||||||
|
**Что реализуем:**
|
||||||
|
- Air resistance и terminal velocity
|
||||||
|
- Wind/updraft systems
|
||||||
|
- Gliding механика
|
||||||
|
- Landing impact detection и анимации
|
||||||
- Air-to-ground transition smoothing
|
- Air-to-ground transition smoothing
|
||||||
- Landing impact (приседание при жёстком приземлении)
|
- Вывод необходимых значений в Debug HUD
|
||||||
|
- Вывод результатов тестов в HUD
|
||||||
|
|
||||||
**Критерии:**
|
**Критерии успеха:**
|
||||||
- [ ] Прыжок ощущается "сочно" (Mario-like arc)
|
- ✅ Реалистичная траектория полета
|
||||||
- [ ] Плавные приземления
|
- ✅ Плавные приземления без "хлопков"
|
||||||
- [ ] Предсказуемая траектория
|
- ✅ Terminal velocity ограничивает падение
|
||||||
|
- ✅ Responsive air control без нарушения физики
|
||||||
**Время:** ~3-4 часа
|
- ✅ Значения корректно отображаются в Debug HUD
|
||||||
|
- ✅ Результаты тестов отображаются в HUD
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# 🟡 ФАЗА 2: LEVEL MECHANICS (Прототип "Подвал")
|
# Этап 19: Продвинутые склоны и поверхности
|
||||||
> Минимум для прохождения уровня от начала до конца
|
**Цель:** Сложные взаимодействия с геометрией
|
||||||
|
**Результат:** Разнообразные типы поверхностей
|
||||||
|
|
||||||
|
**Что реализуем:**
|
||||||
|
- Ice surfaces (скользкие поверхности с инерцией)
|
||||||
|
- Conveyor belts (движущиеся платформы)
|
||||||
|
- Bouncy surfaces (отскакивающие поверхности)
|
||||||
|
- Sticky surfaces (замедляющие движение)
|
||||||
|
- Slope acceleration/deceleration physics
|
||||||
|
- Вывод необходимых значений в Debug HUD
|
||||||
|
- Вывод результатов тестов в HUD
|
||||||
|
|
||||||
|
**Критерии успеха:**
|
||||||
|
- ✅ Каждый тип поверхности ощущается уникально
|
||||||
|
- ✅ Плавные переходы между типами поверхностей
|
||||||
|
- ✅ Детерминированное поведение всех типов
|
||||||
|
- ✅ Легкая настройка параметров поверхностей
|
||||||
|
- ✅ Значения корректно отображаются в Debug HUD
|
||||||
|
- ✅ Результаты тестов отображаются в HUD
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Этап 15: Система подбора предметов
|
# Этап 20: Wall interactions
|
||||||
**Демо:** Подобрать камешек/кость, держать, положить
|
|
||||||
**Нужно для:** Бросок в свечу
|
|
||||||
|
|
||||||
**Реализация:**
|
|
||||||
- Pickup radius detection
|
|
||||||
- Inventory slot (1 предмет в руках)
|
|
||||||
- Hold/drop mechanics
|
|
||||||
- Visual attachment к персонажу
|
|
||||||
- Item data asset (вес, throwable flag)
|
|
||||||
|
|
||||||
**Критерии:**
|
|
||||||
- [ ] Подбор в радиусе ~100см
|
|
||||||
- [ ] Предмет визуально в руке
|
|
||||||
- [ ] Drop кладёт под ноги
|
|
||||||
|
|
||||||
**Время:** ~3-4 часа
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Этап 16: Система бросков
|
|
||||||
**Демо:** Кинуть камешек в свечу → свеча качается
|
|
||||||
**Нужно для:** Активация раскачивания
|
|
||||||
|
|
||||||
**Реализация:**
|
|
||||||
- Aim trajectory preview (параболическая линия)
|
|
||||||
- Throw force (зажатие = сильнее)
|
|
||||||
- Projectile physics
|
|
||||||
- Impact detection → события
|
|
||||||
- Throwable interface
|
|
||||||
|
|
||||||
**Критерии:**
|
|
||||||
- [ ] Траектория предсказуема
|
|
||||||
- [ ] Попадание триггерит события
|
|
||||||
- [ ] Можно подобрать снова
|
|
||||||
|
|
||||||
**Время:** ~4-5 часов
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Этап 17: Интерактивные объекты
|
|
||||||
**Демо:** Дёрнуть кольцо, открыть дверцу шкафа
|
|
||||||
**Нужно для:** Все механизмы уровня
|
|
||||||
|
|
||||||
**Реализация:**
|
|
||||||
- IInteractable interface
|
|
||||||
- Interaction prompt UI (E / кнопка геймпада)
|
|
||||||
- State machine (open/closed, on/off)
|
|
||||||
- Single-use vs reusable
|
|
||||||
- Audio/visual feedback
|
|
||||||
|
|
||||||
**Критерии:**
|
|
||||||
- [ ] Prompt появляется в радиусе
|
|
||||||
- [ ] Состояния сохраняются
|
|
||||||
- [ ] Работает keyboard + gamepad
|
|
||||||
|
|
||||||
**Время:** ~3-4 часа
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Этап 18: Физика качания (Pendulum)
|
|
||||||
**Демо:** Свеча качается, клетка качается синхронно
|
|
||||||
**Нужно для:** Побег из клетки, тарзанка
|
|
||||||
|
|
||||||
**Реализация:**
|
|
||||||
- Pendulum physics (угол, длина, затухание)
|
|
||||||
- Player-induced swing (нажатия в такт)
|
|
||||||
- Swing transfer (синхронизация объектов)
|
|
||||||
- Impact trigger (свеча → верёвка)
|
|
||||||
|
|
||||||
**Критерии:**
|
|
||||||
- [ ] Физика реалистична
|
|
||||||
- [ ] Раскачка интуитивна
|
|
||||||
- [ ] Объекты синхронизируются
|
|
||||||
|
|
||||||
**Время:** ~5-6 часов
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Этап 19: Система огня
|
|
||||||
**Демо:** Свеча поджигает верёвку → верёвка сгорает
|
|
||||||
**Нужно для:** Падение клетки
|
|
||||||
|
|
||||||
**Реализация:**
|
|
||||||
- Fire source component
|
|
||||||
- Flammable component
|
|
||||||
- Burn duration → destruction
|
|
||||||
- Visual fire effect (Niagara)
|
|
||||||
- Fire spread (опционально)
|
|
||||||
|
|
||||||
**Критерии:**
|
|
||||||
- [ ] Поджог требует контакта
|
|
||||||
- [ ] Горение → разрушение
|
|
||||||
- [ ] Визуально убедительно
|
|
||||||
|
|
||||||
**Время:** ~4-5 часов
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Этап 20: Система верёвки (Rope)
|
|
||||||
**Демо:** Накинуть верёвку на крюк, качнуться через котёл
|
|
||||||
**Нужно для:** Тарзанка
|
|
||||||
|
|
||||||
**Реализация:**
|
|
||||||
- Rope throw targeting
|
|
||||||
- Attach point detection
|
|
||||||
- Rope physics (длина, натяжение)
|
|
||||||
- Swing while attached (использует Pendulum)
|
|
||||||
- Dismount с сохранением momentum
|
|
||||||
|
|
||||||
**Критерии:**
|
|
||||||
- [ ] Верёвка цепляется за валидные точки
|
|
||||||
- [ ] Качание как pendulum
|
|
||||||
- [ ] Отпустить = сохранить скорость
|
|
||||||
|
|
||||||
**Время:** ~6-8 часов
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Этап 21: Система временных бафов
|
|
||||||
**Демо:** Выпить зелье → прыжок выше на 10 сек
|
|
||||||
**Нужно для:** Прыгучее зелье
|
|
||||||
|
|
||||||
**Реализация:**
|
|
||||||
- Buff data asset (тип, длительность, множитель)
|
|
||||||
- Active buffs container
|
|
||||||
- Buff UI (иконка + таймер)
|
|
||||||
- Stat modification
|
|
||||||
- Consumable integration
|
|
||||||
|
|
||||||
**Критерии:**
|
|
||||||
- [ ] Визуальный эффект на персонаже
|
|
||||||
- [ ] Таймер виден
|
|
||||||
- [ ] Эффект заканчивается плавно
|
|
||||||
|
|
||||||
**Время:** ~4-5 часов
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Этап 22: Головоломка с зеркалом
|
|
||||||
**Демо:** Шкаф невидим напрямую, виден в отражении
|
|
||||||
**Нужно для:** Шкаф с зельями
|
|
||||||
|
|
||||||
**Реализация:**
|
|
||||||
- View frustum check
|
|
||||||
- Mirror render target
|
|
||||||
- Conditional visibility
|
|
||||||
- "Blind interaction"
|
|
||||||
- Shimmer effect (намёк)
|
|
||||||
|
|
||||||
**Критерии:**
|
|
||||||
- [ ] Исчезает при прямом взгляде
|
|
||||||
- [ ] Виден в зеркале
|
|
||||||
- [ ] Можно взаимодействовать вслепую
|
|
||||||
|
|
||||||
**Время:** ~6-8 часов
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Этап 23: Складные механизмы
|
|
||||||
**Демо:** Дёрнуть кольцо → лестница разворачивается
|
|
||||||
**Нужно для:** Лестница к люку
|
|
||||||
|
|
||||||
**Реализация:**
|
|
||||||
- Deployable base class
|
|
||||||
- Trigger mechanisms (кольцо, бросок)
|
|
||||||
- Deployment animation
|
|
||||||
- Safety check (не придавить игрока)
|
|
||||||
|
|
||||||
**Критерии:**
|
|
||||||
- [ ] Активация от разных источников
|
|
||||||
- [ ] Плавная анимация
|
|
||||||
- [ ] Можно использовать после deployment
|
|
||||||
|
|
||||||
**Время:** ~4-5 часов
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Этап 24: Взбирание (Climbing)
|
|
||||||
**Демо:** Залезть по лестнице к люку
|
|
||||||
**Нужно для:** Финальный выход
|
|
||||||
|
|
||||||
**Реализация:**
|
|
||||||
- Climbable surface detection
|
|
||||||
- Climb state machine
|
|
||||||
- Climb movement (вверх/вниз)
|
|
||||||
- Dismount (вверху, внизу, в сторону)
|
|
||||||
|
|
||||||
**Критерии:**
|
|
||||||
- [ ] Автоцепляние к лестнице
|
|
||||||
- [ ] Плавное движение
|
|
||||||
- [ ] Выход наверху без рывков
|
|
||||||
|
|
||||||
**Время:** ~4-5 часов
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 🟢 ФАЗА 3: POLISH & ADVANCED MOVEMENT
|
|
||||||
> После играбельного прототипа
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Этап 25: Профессиональная камера
|
|
||||||
**Цель:** AAA-уровень камеры
|
|
||||||
|
|
||||||
**Реализация:**
|
|
||||||
- Camera lag и damping
|
|
||||||
- Look-ahead prediction
|
|
||||||
- Dead zone для микродвижений
|
|
||||||
- Collision avoidance (sphere trace)
|
|
||||||
- Character highlight когда за препятствием
|
|
||||||
|
|
||||||
**Критерии:**
|
|
||||||
- [ ] Нет рывков при старте/остановке
|
|
||||||
- [ ] Камера не проваливается в стены
|
|
||||||
- [ ] Плавные повороты
|
|
||||||
|
|
||||||
**Время:** ~5-6 часов
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Этап 26: Wall interactions
|
|
||||||
**Цель:** Продвинутые взаимодействия со стенами
|
**Цель:** Продвинутые взаимодействия со стенами
|
||||||
|
**Результат:** Wall jumping, wall sliding, wall climbing
|
||||||
|
|
||||||
**Реализация:**
|
**Что реализуем:**
|
||||||
- Wall jumping с momentum
|
- Wall jumping с momentum preservation
|
||||||
- Wall sliding с контролем скорости
|
- Wall sliding с контролем скорости
|
||||||
- Corner grabbing
|
- Wall climbing на специальных поверхностях
|
||||||
- Ledge detection
|
- Corner grabbing и edge detection
|
||||||
|
- Wall run система (опционально)
|
||||||
|
- Вывод необходимых значений в Debug HUD
|
||||||
|
- Вывод результатов тестов в HUD
|
||||||
|
|
||||||
**Критерии:**
|
**Критерии успеха:**
|
||||||
- [ ] Responsive wall jump
|
- ✅ Responsive wall jumping с правильными углами
|
||||||
- [ ] Smooth transitions wall ↔ ground ↔ air
|
- ✅ Контролируемое wall sliding
|
||||||
|
- ✅ Smooth transitions wall ↔ ground ↔ air
|
||||||
**Время:** ~6-8 часов
|
- ✅ Интуитивное управление wall mechanics
|
||||||
|
- ✅ Значения корректно отображаются в Debug HUD
|
||||||
|
- ✅ Результаты тестов отображаются в HUD
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Этап 27: Специальные движения
|
# Этап 21: Специальные движения
|
||||||
**Цель:** Dash, ground pound, ledge grab
|
**Цель:** Уникальные движения для богатого геймплея
|
||||||
|
**Результат:** Dash, ground pound, ledge grab и другие
|
||||||
|
|
||||||
**Реализация:**
|
**Что реализуем:**
|
||||||
- Dash/dodge с i-frames
|
- Dash/dodge с invincibility frames
|
||||||
- Ground pound с area impact
|
- Ground pound с area impact
|
||||||
- Ledge grabbing и climbing
|
- Ledge grabbing и climbing
|
||||||
- Slide/crouch
|
- Slide/crouch движения
|
||||||
|
- Special movement abilities
|
||||||
|
- Вывод необходимых значений в Debug HUD
|
||||||
|
- Вывод результатов тестов в HUD
|
||||||
|
|
||||||
**Критерии:**
|
**Критерии успеха:**
|
||||||
- [ ] Каждое движение impactful
|
- ✅ Каждое движение ощущается impact-ful
|
||||||
- [ ] Smooth combinations
|
- ✅ Smooth combinations между движениями
|
||||||
|
- ✅ Balanced timing и cooldowns
|
||||||
**Время:** ~6-8 часов
|
- ✅ Clear visual и audio feedback
|
||||||
|
- ✅ Значения корректно отображаются в Debug HUD
|
||||||
|
- ✅ Результаты тестов отображаются в HUD
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Этап 28: Продвинутые поверхности
|
# Этап 22: Performance optimization
|
||||||
**Цель:** Разнообразие типов поверхностей
|
**Цель:** 60 FPS на целевом железе в любых сценариях
|
||||||
|
**Результат:** Оптимизированная система коллизий
|
||||||
|
|
||||||
**Реализация:**
|
**Что реализуем:**
|
||||||
- Ice (скользкие)
|
- Spatial partitioning для collision objects
|
||||||
- Conveyor belts
|
- LOD system для collision complexity
|
||||||
- Bouncy surfaces
|
- Multi-threading collision checks
|
||||||
- Sticky surfaces
|
- Memory pool для collision queries
|
||||||
|
- Predictive collision culling
|
||||||
|
- Вывод необходимых значений в Debug HUD
|
||||||
|
- Вывод результатов тестов в HUD
|
||||||
|
|
||||||
**Критерии:**
|
**Критерии успеха:**
|
||||||
- [ ] Каждый тип уникален
|
- ✅ Stable 60+ FPS на целевом железе
|
||||||
- [ ] Плавные переходы между типами
|
- ✅ <5ms на collision detection в worst case
|
||||||
|
- ✅ Scalable performance до 100+ collision objects
|
||||||
**Время:** ~5-6 часов
|
- ✅ Minimal memory allocations в runtime
|
||||||
|
- ✅ Значения корректно отображаются в Debug HUD
|
||||||
|
- ✅ Результаты тестов отображаются в HUD
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Этап 29: Audio система
|
# Этап 23: Debug и профилирование tools
|
||||||
**Цель:** Звуковой feedback
|
**Цель:** Профессиональные инструменты для тонкой настройки
|
||||||
|
**Результат:** Полный контроль над системой
|
||||||
|
|
||||||
**Реализация:**
|
**Что реализуем:**
|
||||||
- Footstep sounds (по типу поверхности)
|
- Visual collision debugging (rays, sweeps, contacts)
|
||||||
- Jump/land sounds
|
- Real-time performance metrics и profiling
|
||||||
- Interaction sounds
|
- Tweakable parameters в runtime через UI
|
||||||
- Ambient audio
|
- Automated testing suite для regression testing
|
||||||
|
|
||||||
**Время:** ~4-5 часов
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Этап 30: Debug и профилирование
|
|
||||||
**Цель:** Инструменты для тонкой настройки
|
|
||||||
|
|
||||||
**Реализация:**
|
|
||||||
- Visual collision debugging
|
|
||||||
- Runtime tweakable parameters
|
|
||||||
- Performance metrics
|
|
||||||
- Replay system для детерминированности
|
- Replay system для детерминированности
|
||||||
|
- Вывод необходимых значений в Debug HUD
|
||||||
|
- Вывод результатов тестов в HUD
|
||||||
|
|
||||||
**Время:** ~4-5 часов
|
**Критерии успеха:**
|
||||||
|
- ✅ Visual debugging показывает все collision queries
|
||||||
|
- ✅ Real-time параметры настраиваются без restart
|
||||||
|
- ✅ Performance metrics точные и useful
|
||||||
|
- ✅ Automated tests покрывают все основные сценарии
|
||||||
|
- ✅ Значения корректно отображаются в Debug HUD
|
||||||
|
- ✅ Результаты тестов отображаются в HUD
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Этап 31: Edge cases и stress testing
|
# Этап 24: Edge cases и stress testing
|
||||||
**Цель:** Bullet-proof система
|
**Цель:** Bullet-proof система для любых условий
|
||||||
|
**Результат:** Система работает в экстремальных сценариях
|
||||||
|
|
||||||
**Реализация:**
|
**Что реализуем:**
|
||||||
- Extreme velocity testing
|
- Extreme velocity testing (10000+ units/sec)
|
||||||
- Complex geometry stress tests
|
- Complex geometry stress tests
|
||||||
- Memory leak detection
|
- Memory leak detection и prevention
|
||||||
- NaN/infinity handling
|
- Determinism verification tools
|
||||||
|
- Edge case handling (NaN, infinity, etc.)
|
||||||
|
- Вывод необходимых значений в Debug HUD
|
||||||
|
- Вывод результатов тестов в HUD
|
||||||
|
|
||||||
**Время:** ~3-4 часа
|
**Критерии успеха:**
|
||||||
|
- ✅ Система не ломается при экстремальных значениях
|
||||||
|
- ✅ No memory leaks при длительной работе
|
||||||
|
- ✅ Determinism поддерживается в любых условиях
|
||||||
|
- ✅ Graceful degradation при перегрузке
|
||||||
|
- ✅ Значения корректно отображаются в Debug HUD
|
||||||
|
- ✅ Результаты тестов отображаются в HUD
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Этап 32: UX Polish
|
# Этап 25: User experience polish
|
||||||
**Цель:** Commercial game feel
|
**Время:** 3-4 дня | **Сложность:** Средняя
|
||||||
|
**Цель:** Finalized user experience
|
||||||
|
**Результат:** Система ощущается как в коммерческой игре
|
||||||
|
|
||||||
**Реализация:**
|
**Что реализуем:**
|
||||||
- Input buffering refinement
|
- Input buffering и prediction
|
||||||
- Haptic feedback (gamepad)
|
- Haptic feedback integration (геймпады)
|
||||||
- Visual effects (dust, particles)
|
- Audio feedback integration для movement
|
||||||
|
- Visual effects integration (dust, particles)
|
||||||
- Accessibility options
|
- Accessibility options
|
||||||
|
- Вывод необходимых значений в Debug HUD
|
||||||
|
- Вывод результатов тестов в HUD
|
||||||
|
|
||||||
**Время:** ~4-5 часов
|
**Критерии успеха:**
|
||||||
|
- ✅ Controls ощущаются максимально responsive
|
||||||
|
- ✅ Rich feedback для всех действий
|
||||||
|
- ✅ Поддержка различных input методов
|
||||||
|
- ✅ Accessibility options работают корректно
|
||||||
|
- ✅ Значения корректно отображаются в Debug HUD
|
||||||
|
- ✅ Результаты тестов отображаются в HUD
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# 🔵 ФАЗА 4: CONTENT
|
|
||||||
> После полировки движения
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Этап 33: Level transition
|
|
||||||
Переход люк → кухня
|
|
||||||
|
|
||||||
## Этап 34: Save/Checkpoint
|
|
||||||
Сохранение прогресса
|
|
||||||
|
|
||||||
## Этап 35: Второй уровень
|
|
||||||
Кухня первого этажа
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ЗАВИСИМОСТИ
|
|
||||||
|
|
||||||
```
|
|
||||||
ФАЗА 1 (Core Movement)
|
|
||||||
[13: Прыжки] ──► [14: Воздушная физика]
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
ФАЗА 2 (Level Mechanics)
|
|
||||||
│
|
|
||||||
├──► [15: Подбор] ──► [16: Броски] ───┐
|
|
||||||
│ │
|
|
||||||
├──► [17: Интерактивы] ───────────────┤
|
|
||||||
│ │
|
|
||||||
├──► [18: Качание] ──► [19: Огонь] ───┼──► КЛЕТКА
|
|
||||||
│ │ │
|
|
||||||
│ └──► [20: Верёвка] ──────────┼──► ТАРЗАНКА
|
|
||||||
│ │
|
|
||||||
├──► [21: Бафы] ──┬──► [22: Зеркало] ──┼──► ШКАФ
|
|
||||||
│ │ │
|
|
||||||
└──► [23: Механизмы] ──► [24: Climbing]┴──► ЛЮКЙ
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ОЦЕНКА ВРЕМЕНИ
|
|
||||||
|
|
||||||
| Фаза | Этапы | Часы | Результат |
|
|
||||||
|------|-------|------|-----------|
|
|
||||||
| 1: Core | 13-14 | ~7-10 | Прыжки работают |
|
|
||||||
| 2: Level | 15-24 | ~44-58 | Уровень проходим |
|
|
||||||
| 3: Polish | 25-32 | ~37-47 | AAA feel |
|
|
||||||
| 4: Content | 33-35 | ~10-15 | Больше контента |
|
|
||||||
| **Итого** | | **~98-130** | |
|
|
||||||
|
|
||||||
**MVP (Фазы 1-2):** ~51-68 часов = **13-17 дней** (при 4ч/день)
|
|
||||||
**Polished (+ Фаза 3):** ~88-115 часов = **22-29 дней**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## КРИТИЧЕСКИЙ ПУТЬ ДО MVP
|
|
||||||
|
|
||||||
```
|
|
||||||
13 → 14 → 15 → 16 → 17 → 18 → 19 → 20 → 21 → 22 → 23 → 24
|
|
||||||
▲
|
|
||||||
│
|
|
||||||
START HERE
|
|
||||||
```
|
|
||||||
|
|
||||||
Каждый этап даёт демонстрируемый результат.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ПРИМЕЧАНИЯ
|
|
||||||
|
|
||||||
1. **Debug HUD** — используем UE инструменты (Gameplay Debugger, etc.)
|
|
||||||
2. **Тесты** — отложены до Фазы 3
|
|
||||||
3. **C++** — продолжаем использовать для производительности
|
|
||||||
4. **Итерации** — после каждого этапа можно показать прогресс
|
|
||||||
|
|
|
||||||
|
|
@ -1,343 +0,0 @@
|
||||||
// Request Games © All rights reserved
|
|
||||||
|
|
||||||
// Source/TengriPlatformer/Character/TengriCharacter.cpp
|
|
||||||
|
|
||||||
#include "TengriCharacter.h"
|
|
||||||
#include "Components/CapsuleComponent.h"
|
|
||||||
#include "Components/SkeletalMeshComponent.h"
|
|
||||||
#include "Components/ArrowComponent.h"
|
|
||||||
#include "TengriPlatformer/Movement/TengriMovementComponent.h"
|
|
||||||
#include "TengriPlatformer/World/TengriPickupActor.h"
|
|
||||||
#include "Kismet/KismetSystemLibrary.h"
|
|
||||||
|
|
||||||
// Enhanced Input
|
|
||||||
#include "EnhancedInputComponent.h"
|
|
||||||
#include "EnhancedInputSubsystems.h"
|
|
||||||
#include "InputMappingContext.h"
|
|
||||||
|
|
||||||
DEFINE_LOG_CATEGORY_STATIC(LogTengriCharacter, Log, All);
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// CONSTANTS
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
namespace TengriCharacter
|
|
||||||
{
|
|
||||||
// Camera rotation speed multiplier in strafe mode
|
|
||||||
constexpr float StrafeRotationSpeedMultiplier = 2.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// INITIALIZATION
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
ATengriCharacter::ATengriCharacter()
|
|
||||||
{
|
|
||||||
PrimaryActorTick.bCanEverTick = true;
|
|
||||||
|
|
||||||
// Setup collision capsule
|
|
||||||
CapsuleComponent = CreateDefaultSubobject<UCapsuleComponent>(TEXT("CapsuleComponent"));
|
|
||||||
CapsuleComponent->InitCapsuleSize(34.0f, 88.0f);
|
|
||||||
CapsuleComponent->SetCollisionProfileName(UCollisionProfile::Pawn_ProfileName);
|
|
||||||
RootComponent = CapsuleComponent;
|
|
||||||
|
|
||||||
// Setup debug arrow
|
|
||||||
ArrowComponent = CreateDefaultSubobject<UArrowComponent>(TEXT("Arrow"));
|
|
||||||
ArrowComponent->SetupAttachment(CapsuleComponent);
|
|
||||||
ArrowComponent->SetRelativeLocation(FVector(0.f, 0.f, 50.f));
|
|
||||||
|
|
||||||
// Setup character mesh
|
|
||||||
Mesh = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("Mesh"));
|
|
||||||
Mesh->SetupAttachment(CapsuleComponent);
|
|
||||||
Mesh->SetRelativeLocation(FVector(0.0f, 0.0f, -88.0f));
|
|
||||||
Mesh->SetRelativeRotation(FRotator(0.0f, -90.0f, 0.0f));
|
|
||||||
|
|
||||||
// Setup custom movement component
|
|
||||||
MovementComponent = CreateDefaultSubobject<UTengriMovementComponent>(TEXT("TengriMovement"));
|
|
||||||
}
|
|
||||||
|
|
||||||
void ATengriCharacter::BeginPlay()
|
|
||||||
{
|
|
||||||
Super::BeginPlay();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// INPUT SETUP
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
void ATengriCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
|
|
||||||
{
|
|
||||||
Super::SetupPlayerInputComponent(PlayerInputComponent);
|
|
||||||
|
|
||||||
UEnhancedInputComponent* EnhancedInput = Cast<UEnhancedInputComponent>(PlayerInputComponent);
|
|
||||||
if (!EnhancedInput)
|
|
||||||
{
|
|
||||||
UE_LOG(LogTengriCharacter, Error,
|
|
||||||
TEXT("SetupPlayerInputComponent: Enhanced Input not available"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bind Interact action (always active via IMC_Default)
|
|
||||||
if (InteractAction)
|
|
||||||
{
|
|
||||||
EnhancedInput->BindAction(InteractAction, ETriggerEvent::Started,
|
|
||||||
this, &ATengriCharacter::Interact);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bind Throw action (enabled via IMC_ItemHeld when item equipped)
|
|
||||||
if (ThrowAction)
|
|
||||||
{
|
|
||||||
EnhancedInput->BindAction(ThrowAction, ETriggerEvent::Started,
|
|
||||||
this, &ATengriCharacter::OnThrowInput);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bind Aim action (enabled via IMC_ItemHeld when item equipped)
|
|
||||||
if (AimAction)
|
|
||||||
{
|
|
||||||
EnhancedInput->BindAction(AimAction, ETriggerEvent::Started,
|
|
||||||
this, &ATengriCharacter::OnAimInput);
|
|
||||||
EnhancedInput->BindAction(AimAction, ETriggerEvent::Completed,
|
|
||||||
this, &ATengriCharacter::OnAimInput);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ATengriCharacter::ToggleItemHeldContext(const bool bEnable) const
|
|
||||||
{
|
|
||||||
const APlayerController* PC = Cast<APlayerController>(GetController());
|
|
||||||
if (!PC)
|
|
||||||
{
|
|
||||||
UE_LOG(LogTengriCharacter, Warning,
|
|
||||||
TEXT("ToggleItemHeldContext: No player controller"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
UEnhancedInputLocalPlayerSubsystem* Subsystem =
|
|
||||||
ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PC->GetLocalPlayer());
|
|
||||||
|
|
||||||
if (!Subsystem)
|
|
||||||
{
|
|
||||||
UE_LOG(LogTengriCharacter, Error,
|
|
||||||
TEXT("ToggleItemHeldContext: Enhanced Input subsystem not available"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ItemHeldMappingContext)
|
|
||||||
{
|
|
||||||
UE_LOG(LogTengriCharacter, Warning,
|
|
||||||
TEXT("ToggleItemHeldContext: ItemHeldMappingContext not assigned"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bEnable)
|
|
||||||
{
|
|
||||||
// Add item-equipped context (Priority 1 overrides default controls)
|
|
||||||
Subsystem->AddMappingContext(ItemHeldMappingContext, 1);
|
|
||||||
UE_LOG(LogTengriCharacter, Verbose, TEXT("Item context enabled"));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Remove item context (revert to default movement controls)
|
|
||||||
Subsystem->RemoveMappingContext(ItemHeldMappingContext);
|
|
||||||
UE_LOG(LogTengriCharacter, Verbose, TEXT("Item context disabled"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// INTERACTION LOGIC
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
void ATengriCharacter::Interact()
|
|
||||||
{
|
|
||||||
// SCENARIO 1: Holding item -> Drop gently
|
|
||||||
if (HeldItem)
|
|
||||||
{
|
|
||||||
const FVector GentleImpulse = GetActorForwardVector() * GentleDropSpeed;
|
|
||||||
|
|
||||||
// Apply gentle impulse (bVelChange = true ignores mass)
|
|
||||||
HeldItem->OnDropped(GentleImpulse, true);
|
|
||||||
HeldItem = nullptr;
|
|
||||||
|
|
||||||
// Disable combat mode controls
|
|
||||||
ToggleItemHeldContext(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// SCENARIO 2: Empty hands -> Pick up nearest item
|
|
||||||
if (ATengriPickupActor* FoundItem = FindNearestPickup())
|
|
||||||
{
|
|
||||||
HeldItem = FoundItem;
|
|
||||||
HeldItem->OnPickedUp(Mesh, HandSocketName);
|
|
||||||
|
|
||||||
// Enable combat mode controls (Throw/Aim now active)
|
|
||||||
ToggleItemHeldContext(true);
|
|
||||||
|
|
||||||
UE_LOG(LogTengriCharacter, Verbose,
|
|
||||||
TEXT("Picked up item: %s"), *FoundItem->GetName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ATengriCharacter::OnThrowInput()
|
|
||||||
{
|
|
||||||
if (!HeldItem)
|
|
||||||
{
|
|
||||||
UE_LOG(LogTengriCharacter, Warning,
|
|
||||||
TEXT("OnThrowInput: No item held"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default to forward direction if raycasting fails
|
|
||||||
FVector ThrowDirection = GetActorForwardVector();
|
|
||||||
|
|
||||||
// ИСПРАВЛЕНИЕ: Переименовали Controller -> PC, чтобы избежать конфликта имен
|
|
||||||
AController* PC = GetController();
|
|
||||||
|
|
||||||
if (UWorld* World = GetWorld(); !PC || !World)
|
|
||||||
{
|
|
||||||
UE_LOG(LogTengriCharacter, Warning,
|
|
||||||
TEXT("OnThrowInput: Invalid controller or world context"));
|
|
||||||
// Continue with fallback direction
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// ─────────────────────────────────────────────────────────────────
|
|
||||||
// PRECISE THROW TRAJECTORY CALCULATION
|
|
||||||
// ─────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
// Get camera position and rotation
|
|
||||||
FVector CameraLoc;
|
|
||||||
FRotator CameraRot;
|
|
||||||
PC->GetPlayerViewPoint(CameraLoc, CameraRot); // Используем PC вместо Controller
|
|
||||||
|
|
||||||
// Raycast from camera forward
|
|
||||||
const FVector TraceStart = CameraLoc;
|
|
||||||
const FVector TraceEnd = CameraLoc + (CameraRot.Vector() * ThrowTraceDistance);
|
|
||||||
|
|
||||||
FHitResult Hit;
|
|
||||||
FCollisionQueryParams QueryParams;
|
|
||||||
QueryParams.AddIgnoredActor(this);
|
|
||||||
QueryParams.AddIgnoredActor(HeldItem);
|
|
||||||
|
|
||||||
// Find world target point (wall/enemy/floor where camera is looking)
|
|
||||||
const bool bHit = World->LineTraceSingleByChannel(
|
|
||||||
Hit,
|
|
||||||
TraceStart,
|
|
||||||
TraceEnd,
|
|
||||||
ECC_Visibility,
|
|
||||||
QueryParams
|
|
||||||
);
|
|
||||||
|
|
||||||
// Use hit point if found, otherwise use far endpoint
|
|
||||||
const FVector TargetPoint = bHit ? Hit.ImpactPoint : TraceEnd;
|
|
||||||
|
|
||||||
// Calculate throw direction FROM item TO target
|
|
||||||
const FVector HandLocation = HeldItem->GetActorLocation();
|
|
||||||
ThrowDirection = (TargetPoint - HandLocation).GetSafeNormal();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add arc elevation for natural parabolic trajectory
|
|
||||||
ThrowDirection += FVector(0.0f, 0.0f, ThrowArcElevation);
|
|
||||||
ThrowDirection.Normalize();
|
|
||||||
|
|
||||||
// Rotate character instantly to face throw direction
|
|
||||||
FRotator CharacterFaceRot = ThrowDirection.Rotation();
|
|
||||||
CharacterFaceRot.Pitch = 0.0f;
|
|
||||||
CharacterFaceRot.Roll = 0.0f;
|
|
||||||
|
|
||||||
if (MovementComponent)
|
|
||||||
{
|
|
||||||
MovementComponent->ForceRotation(CharacterFaceRot);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute throw with calculated trajectory
|
|
||||||
const FVector Impulse = ThrowDirection * ThrowForce;
|
|
||||||
HeldItem->OnDropped(Impulse, true);
|
|
||||||
HeldItem = nullptr;
|
|
||||||
|
|
||||||
// Disable combat controls
|
|
||||||
ToggleItemHeldContext(false);
|
|
||||||
|
|
||||||
UE_LOG(LogTengriCharacter, Verbose,
|
|
||||||
TEXT("Threw item with force: %.1f cm/s"), ThrowForce);
|
|
||||||
|
|
||||||
// Note: bIsAiming state will be cleared by OnAimInput when player releases RMB/L2
|
|
||||||
}
|
|
||||||
|
|
||||||
void ATengriCharacter::OnAimInput(const FInputActionValue& Value)
|
|
||||||
{
|
|
||||||
// Extract boolean state (true = button pressed, false = released)
|
|
||||||
const bool bIsPressed = Value.Get<bool>();
|
|
||||||
|
|
||||||
// Skip if state hasn't changed (optimization)
|
|
||||||
if (bIsPressed == bIsAiming)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
bIsAiming = bIsPressed;
|
|
||||||
|
|
||||||
// Sync strafe mode with movement component
|
|
||||||
if (MovementComponent)
|
|
||||||
{
|
|
||||||
MovementComponent->SetStrafing(bIsAiming);
|
|
||||||
UE_LOG(LogTengriCharacter, Verbose,
|
|
||||||
TEXT("Aim mode: %s"), bIsAiming ? TEXT("ON") : TEXT("OFF"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// PICKUP DETECTION
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
ATengriPickupActor* ATengriCharacter::FindNearestPickup() const
|
|
||||||
{
|
|
||||||
UWorld* World = GetWorld();
|
|
||||||
if (!World)
|
|
||||||
{
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
const FVector SpherePos = GetActorLocation();
|
|
||||||
|
|
||||||
// Define object types to detect (physics objects and dynamic actors)
|
|
||||||
TArray<TEnumAsByte<EObjectTypeQuery>> ObjectTypes;
|
|
||||||
ObjectTypes.Add(UEngineTypes::ConvertToObjectType(ECC_PhysicsBody));
|
|
||||||
ObjectTypes.Add(UEngineTypes::ConvertToObjectType(ECC_WorldDynamic));
|
|
||||||
|
|
||||||
// Ignore self in overlap query
|
|
||||||
TArray<AActor*> IgnoreActors;
|
|
||||||
IgnoreActors.Add(const_cast<ATengriCharacter*>(this));
|
|
||||||
|
|
||||||
// Find all overlapping pickups
|
|
||||||
TArray<AActor*> OverlappingActors;
|
|
||||||
UKismetSystemLibrary::SphereOverlapActors(
|
|
||||||
World,
|
|
||||||
SpherePos,
|
|
||||||
PickupRadius,
|
|
||||||
ObjectTypes,
|
|
||||||
ATengriPickupActor::StaticClass(),
|
|
||||||
IgnoreActors,
|
|
||||||
OverlappingActors
|
|
||||||
);
|
|
||||||
|
|
||||||
// Find nearest un-held pickup
|
|
||||||
ATengriPickupActor* NearestItem = nullptr;
|
|
||||||
float MinDistSq = FMath::Square(PickupRadius);
|
|
||||||
|
|
||||||
for (AActor* Actor : OverlappingActors)
|
|
||||||
{
|
|
||||||
ATengriPickupActor* Item = Cast<ATengriPickupActor>(Actor);
|
|
||||||
if (!Item || Item->IsHeld())
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const float DistSq = FVector::DistSquared(SpherePos, Item->GetActorLocation());
|
|
||||||
if (DistSq < MinDistSq)
|
|
||||||
{
|
|
||||||
MinDistSq = DistSq;
|
|
||||||
NearestItem = Item;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return NearestItem;
|
|
||||||
}
|
|
||||||
|
|
@ -1,161 +0,0 @@
|
||||||
// Request Games © All rights reserved
|
|
||||||
|
|
||||||
// Source/TengriPlatformer/Character/TengriCharacter.h
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
|
||||||
#include "GameFramework/Pawn.h"
|
|
||||||
#include "InputActionValue.h"
|
|
||||||
#include "TengriCharacter.generated.h"
|
|
||||||
|
|
||||||
// Forward declarations
|
|
||||||
class UCapsuleComponent;
|
|
||||||
class USkeletalMeshComponent;
|
|
||||||
class UTengriMovementComponent;
|
|
||||||
class ATengriPickupActor;
|
|
||||||
class UArrowComponent;
|
|
||||||
class UInputMappingContext;
|
|
||||||
class UInputAction;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main player character class with item interaction and throwing mechanics.
|
|
||||||
* Supports dynamic input context switching for item-based actions.
|
|
||||||
*/
|
|
||||||
UCLASS()
|
|
||||||
class TENGRIPLATFORMER_API ATengriCharacter : public APawn
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
ATengriCharacter();
|
|
||||||
|
|
||||||
protected:
|
|
||||||
virtual void BeginPlay() override;
|
|
||||||
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
|
|
||||||
|
|
||||||
public:
|
|
||||||
// ========================================================================
|
|
||||||
// COMPONENTS
|
|
||||||
// ========================================================================
|
|
||||||
|
|
||||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
|
|
||||||
TObjectPtr<UCapsuleComponent> CapsuleComponent;
|
|
||||||
|
|
||||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
|
|
||||||
TObjectPtr<USkeletalMeshComponent> Mesh;
|
|
||||||
|
|
||||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
|
|
||||||
TObjectPtr<UArrowComponent> ArrowComponent;
|
|
||||||
|
|
||||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
|
|
||||||
TObjectPtr<UTengriMovementComponent> MovementComponent;
|
|
||||||
|
|
||||||
// ========================================================================
|
|
||||||
// INPUT CONFIG
|
|
||||||
// ========================================================================
|
|
||||||
|
|
||||||
/** Interact action (E / Square) - Always available */
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
|
|
||||||
TObjectPtr<UInputAction> InteractAction;
|
|
||||||
|
|
||||||
/** Throw action (LMB / R2) - Active only in ItemHeld context */
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
|
|
||||||
TObjectPtr<UInputAction> ThrowAction;
|
|
||||||
|
|
||||||
/** Aim action (RMB / L2) - Active only in ItemHeld context */
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
|
|
||||||
TObjectPtr<UInputAction> AimAction;
|
|
||||||
|
|
||||||
/** Input mapping context for item-equipped state (Priority 1) */
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
|
|
||||||
TObjectPtr<UInputMappingContext> ItemHeldMappingContext;
|
|
||||||
|
|
||||||
// ========================================================================
|
|
||||||
// INTERACTION API
|
|
||||||
// ========================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle interact button (E/Square).
|
|
||||||
* Behavior:
|
|
||||||
* - If holding item: Drop gently with small forward impulse
|
|
||||||
* - If empty hands: Pick up nearest item in radius
|
|
||||||
*/
|
|
||||||
UFUNCTION(BlueprintCallable, Category = "Interaction")
|
|
||||||
void Interact();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute throw action (LMB/R2).
|
|
||||||
* Uses camera-based raycasting for precise trajectory calculation.
|
|
||||||
* Automatically rotates character to face throw direction.
|
|
||||||
* @note Only callable when HeldItem is valid
|
|
||||||
*/
|
|
||||||
void OnThrowInput();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle aim input state changes (RMB/L2).
|
|
||||||
* Toggles strafe mode on MovementComponent for camera-aligned rotation.
|
|
||||||
* @param Value - Input action value (true = pressed, false = released)
|
|
||||||
*/
|
|
||||||
void OnAimInput(const FInputActionValue& Value);
|
|
||||||
|
|
||||||
/** Check if character is currently holding an item */
|
|
||||||
UFUNCTION(BlueprintPure, Category = "Interaction")
|
|
||||||
bool HasItem() const { return HeldItem != nullptr; }
|
|
||||||
|
|
||||||
protected:
|
|
||||||
// ========================================================================
|
|
||||||
// INTERACTION STATE
|
|
||||||
// ========================================================================
|
|
||||||
|
|
||||||
/** Currently held item reference (nullptr if empty-handed) */
|
|
||||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Interaction|State")
|
|
||||||
TObjectPtr<ATengriPickupActor> HeldItem;
|
|
||||||
|
|
||||||
/** Aiming state flag (affects camera and animation) */
|
|
||||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Interaction|State")
|
|
||||||
bool bIsAiming = false;
|
|
||||||
|
|
||||||
// ========================================================================
|
|
||||||
// INTERACTION CONFIG
|
|
||||||
// ========================================================================
|
|
||||||
|
|
||||||
/** Socket name on character mesh for attaching held items */
|
|
||||||
UPROPERTY(EditDefaultsOnly, Category = "Interaction|Config")
|
|
||||||
FName HandSocketName = FName("HandSocket");
|
|
||||||
|
|
||||||
/** Sphere radius for detecting nearby pickups (cm) */
|
|
||||||
UPROPERTY(EditDefaultsOnly, Category = "Interaction|Config")
|
|
||||||
float PickupRadius = 150.0f;
|
|
||||||
|
|
||||||
/** Throw velocity magnitude (cm/s) */
|
|
||||||
UPROPERTY(EditDefaultsOnly, Category = "Interaction|Config")
|
|
||||||
float ThrowForce = 1200.0f;
|
|
||||||
|
|
||||||
/** Gentle drop impulse for interact-to-drop (cm/s) */
|
|
||||||
UPROPERTY(EditDefaultsOnly, Category = "Interaction|Config")
|
|
||||||
float GentleDropSpeed = 50.0f;
|
|
||||||
|
|
||||||
/** Raycast distance for throw targeting (cm) */
|
|
||||||
UPROPERTY(EditDefaultsOnly, Category = "Interaction|Config")
|
|
||||||
float ThrowTraceDistance = 5000.0f;
|
|
||||||
|
|
||||||
/** Arc elevation adjustment for throw trajectory */
|
|
||||||
UPROPERTY(EditDefaultsOnly, Category = "Interaction|Config")
|
|
||||||
float ThrowArcElevation = 0.15f;
|
|
||||||
|
|
||||||
private:
|
|
||||||
/**
|
|
||||||
* Find nearest interactable pickup within radius.
|
|
||||||
* Uses sphere overlap against PhysicsBody and WorldDynamic channels.
|
|
||||||
* @return Nearest valid pickup, or nullptr if none found
|
|
||||||
*/
|
|
||||||
ATengriPickupActor* FindNearestPickup() const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggle ItemHeld input mapping context on/off.
|
|
||||||
* Manages context priority to enable/disable throw and aim actions.
|
|
||||||
* @param bEnable - True to add context, false to remove
|
|
||||||
*/
|
|
||||||
void ToggleItemHeldContext(bool bEnable) const;
|
|
||||||
};
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
// Source/TengriPlatformer/Character/TengriCharacter.ts
|
|
||||||
|
|
||||||
import { Pawn } from '/Content/UE/Pawn.ts';
|
|
||||||
import { CapsuleComponent } from '/Content/UE/CapsuleComponent.ts';
|
|
||||||
import { SkeletalMesh } from '/Content/UE/SkeletalMesh.ts';
|
|
||||||
import { ArrowComponent } from '/Content/UE/ArrowComponent.ts';
|
|
||||||
import { TengriMovementComponent } from '/Source/TengriPlatformer/Movement/TengriMovementComponent.ts';
|
|
||||||
import { InputAction } from '/Content/UE/InputAction.ts';
|
|
||||||
import { InputMappingContext } from '/Content/UE/InputMappingContext.ts';
|
|
||||||
|
|
||||||
export class TengriCharacter extends Pawn {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.CapsuleComponent = new CapsuleComponent();
|
|
||||||
this.Mesh = new SkeletalMesh();
|
|
||||||
this.ArrowComponent = new ArrowComponent();
|
|
||||||
this.MovementComponent = new TengriMovementComponent();
|
|
||||||
|
|
||||||
this.InteractAction = new InputAction();
|
|
||||||
this.ThrowAction = new InputAction();
|
|
||||||
this.AimAction = new InputAction();
|
|
||||||
this.ItemHeldMappingContext = new InputMappingContext();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========================================================================
|
|
||||||
// COMPONENTS
|
|
||||||
// ========================================================================
|
|
||||||
|
|
||||||
public CapsuleComponent: CapsuleComponent;
|
|
||||||
public Mesh: SkeletalMesh;
|
|
||||||
public ArrowComponent: ArrowComponent;
|
|
||||||
public MovementComponent: TengriMovementComponent;
|
|
||||||
|
|
||||||
// ========================================================================
|
|
||||||
// INPUT CONFIG
|
|
||||||
// ========================================================================
|
|
||||||
|
|
||||||
public InteractAction: InputAction;
|
|
||||||
public ThrowAction: InputAction;
|
|
||||||
public AimAction: InputAction;
|
|
||||||
public ItemHeldMappingContext: InputMappingContext;
|
|
||||||
|
|
||||||
// ========================================================================
|
|
||||||
// INTERACTION API
|
|
||||||
// ========================================================================
|
|
||||||
|
|
||||||
public Interact(): void {}
|
|
||||||
public OnItemHeldInput(): void {}
|
|
||||||
|
|
||||||
// ========================================================================
|
|
||||||
// INTERACTION STATE
|
|
||||||
// ========================================================================
|
|
||||||
public bIsAiming: boolean = false;
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
// Request Games © All rights reserved
|
// Source/TengriPlatformer/Movement/Collision/TengriCollisionResolver.cpp
|
||||||
|
|
||||||
// Source/TengriPlatformer/Movement/Collision/TengriCollisionResolver.cpp
|
|
||||||
|
|
||||||
#include "TengriCollisionResolver.h"
|
#include "TengriCollisionResolver.h"
|
||||||
#include "Components/CapsuleComponent.h"
|
#include "Components/CapsuleComponent.h"
|
||||||
|
|
@ -48,7 +46,8 @@ FTengriSweepResult UTengriCollisionResolver::PerformSweep(
|
||||||
const UObject* WorldContext,
|
const UObject* WorldContext,
|
||||||
const FVector& Start,
|
const FVector& Start,
|
||||||
const FVector& End,
|
const FVector& End,
|
||||||
const UCapsuleComponent* Capsule)
|
const UCapsuleComponent* Capsule,
|
||||||
|
bool bShowDebug)
|
||||||
{
|
{
|
||||||
FTengriSweepResult Result;
|
FTengriSweepResult Result;
|
||||||
Result.Location = End;
|
Result.Location = End;
|
||||||
|
|
@ -59,7 +58,7 @@ FTengriSweepResult UTengriCollisionResolver::PerformSweep(
|
||||||
return Result;
|
return Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
const UWorld* World = WorldContext->GetWorld();
|
UWorld* World = WorldContext->GetWorld();
|
||||||
if (!World)
|
if (!World)
|
||||||
{
|
{
|
||||||
return Result;
|
return Result;
|
||||||
|
|
@ -117,7 +116,7 @@ bool UTengriCollisionResolver::StepUp(
|
||||||
const FVector& DesiredDelta,
|
const FVector& DesiredDelta,
|
||||||
const FHitResult& ImpactHit,
|
const FHitResult& ImpactHit,
|
||||||
const UCapsuleComponent* Capsule,
|
const UCapsuleComponent* Capsule,
|
||||||
const float MaxStepHeight,
|
float MaxStepHeight,
|
||||||
FVector& OutLocation)
|
FVector& OutLocation)
|
||||||
{
|
{
|
||||||
// Reject sloped surfaces and overhangs
|
// Reject sloped surfaces and overhangs
|
||||||
|
|
@ -135,7 +134,7 @@ bool UTengriCollisionResolver::StepUp(
|
||||||
|
|
||||||
// === Phase A: Trace Up ===
|
// === Phase A: Trace Up ===
|
||||||
const FVector StepUpEnd = StartLocation + FVector(0.f, 0.f, MaxStepHeight);
|
const FVector StepUpEnd = StartLocation + FVector(0.f, 0.f, MaxStepHeight);
|
||||||
const FTengriSweepResult UpSweep = PerformSweep(WorldContext, StartLocation, StepUpEnd, Capsule);
|
const FTengriSweepResult UpSweep = PerformSweep(WorldContext, StartLocation, StepUpEnd, Capsule, false);
|
||||||
|
|
||||||
// Reject if not enough headroom (hit ceiling before half step height)
|
// Reject if not enough headroom (hit ceiling before half step height)
|
||||||
if (UpSweep.bBlocked && UpSweep.Location.Z < (StartLocation.Z + MaxStepHeight * 0.5f))
|
if (UpSweep.bBlocked && UpSweep.Location.Z < (StartLocation.Z + MaxStepHeight * 0.5f))
|
||||||
|
|
@ -156,7 +155,7 @@ bool UTengriCollisionResolver::StepUp(
|
||||||
const float CheckDist = FMath::Max(DesiredDelta.Size2D(), MinCheckDist);
|
const float CheckDist = FMath::Max(DesiredDelta.Size2D(), MinCheckDist);
|
||||||
const FVector ForwardEnd = UpSweep.Location + (ForwardDir * CheckDist);
|
const FVector ForwardEnd = UpSweep.Location + (ForwardDir * CheckDist);
|
||||||
|
|
||||||
const FTengriSweepResult ForwardSweep = PerformSweep(WorldContext, UpSweep.Location, ForwardEnd, Capsule);
|
const FTengriSweepResult ForwardSweep = PerformSweep(WorldContext, UpSweep.Location, ForwardEnd, Capsule, false);
|
||||||
|
|
||||||
// Reject if obstacle continues upward (wall, next stair step)
|
// Reject if obstacle continues upward (wall, next stair step)
|
||||||
if (ForwardSweep.bBlocked)
|
if (ForwardSweep.bBlocked)
|
||||||
|
|
@ -167,7 +166,7 @@ bool UTengriCollisionResolver::StepUp(
|
||||||
// === Phase C: Trace Down ===
|
// === Phase C: Trace Down ===
|
||||||
const FVector DownStart = ForwardSweep.Location;
|
const FVector DownStart = ForwardSweep.Location;
|
||||||
const FVector DownEnd = DownStart - FVector(0.f, 0.f, MaxStepHeight * TengriPhysics::DownTraceMultiplier);
|
const FVector DownEnd = DownStart - FVector(0.f, 0.f, MaxStepHeight * TengriPhysics::DownTraceMultiplier);
|
||||||
const FTengriSweepResult DownSweep = PerformSweep(WorldContext, DownStart, DownEnd, Capsule);
|
const FTengriSweepResult DownSweep = PerformSweep(WorldContext, DownStart, DownEnd, Capsule, false);
|
||||||
|
|
||||||
if (!DownSweep.bBlocked || DownSweep.Hit.ImpactNormal.Z < TengriPhysics::MinGroundNormalZ)
|
if (!DownSweep.bBlocked || DownSweep.Hit.ImpactNormal.Z < TengriPhysics::MinGroundNormalZ)
|
||||||
{
|
{
|
||||||
|
|
@ -226,7 +225,7 @@ bool UTengriCollisionResolver::SnapToGround(
|
||||||
const FVector Start = Capsule->GetComponentLocation();
|
const FVector Start = Capsule->GetComponentLocation();
|
||||||
const FVector End = Start - FVector(0.f, 0.f, SnapDistance);
|
const FVector End = Start - FVector(0.f, 0.f, SnapDistance);
|
||||||
|
|
||||||
if (const FTengriSweepResult Sweep = PerformSweep(WorldContext, Start, End, Capsule); Sweep.bBlocked && Thresholds.IsWalkable(Sweep.Hit.ImpactNormal.Z))
|
if (const FTengriSweepResult Sweep = PerformSweep(WorldContext, Start, End, Capsule, false); Sweep.bBlocked && Thresholds.IsWalkable(Sweep.Hit.ImpactNormal.Z))
|
||||||
{
|
{
|
||||||
OutLocation = Sweep.Location;
|
OutLocation = Sweep.Location;
|
||||||
OutHit = Sweep.Hit;
|
OutHit = Sweep.Hit;
|
||||||
|
|
@ -277,7 +276,9 @@ namespace
|
||||||
// If wall would push us up, force horizontal movement only
|
// If wall would push us up, force horizontal movement only
|
||||||
if (ClipDelta.Z > 0.f)
|
if (ClipDelta.Z > 0.f)
|
||||||
{
|
{
|
||||||
if (FVector HorizontalTangent = FVector::CrossProduct(ImpactNormal, FVector(0.f, 0.f, 1.f)).GetSafeNormal(); !HorizontalTangent.IsNearlyZero())
|
FVector HorizontalTangent = FVector::CrossProduct(ImpactNormal, FVector(0.f, 0.f, 1.f)).GetSafeNormal();
|
||||||
|
|
||||||
|
if (!HorizontalTangent.IsNearlyZero())
|
||||||
{
|
{
|
||||||
if (FVector::DotProduct(HorizontalTangent, RemainingDelta) < 0.f)
|
if (FVector::DotProduct(HorizontalTangent, RemainingDelta) < 0.f)
|
||||||
{
|
{
|
||||||
|
|
@ -320,7 +321,8 @@ FTengriSweepResult UTengriCollisionResolver::ResolveMovement(
|
||||||
const UCapsuleComponent* Capsule,
|
const UCapsuleComponent* Capsule,
|
||||||
const FSurfaceThresholds& Thresholds,
|
const FSurfaceThresholds& Thresholds,
|
||||||
const float MaxStepHeight,
|
const float MaxStepHeight,
|
||||||
const int32 MaxIterations)
|
const int32 MaxIterations,
|
||||||
|
const bool bShowDebug)
|
||||||
{
|
{
|
||||||
FTengriSweepResult FinalResult;
|
FTengriSweepResult FinalResult;
|
||||||
FinalResult.Location = StartLocation;
|
FinalResult.Location = StartLocation;
|
||||||
|
|
@ -332,7 +334,7 @@ FTengriSweepResult UTengriCollisionResolver::ResolveMovement(
|
||||||
for (int32 Iteration = 0; Iteration < MaxIterations; ++Iteration)
|
for (int32 Iteration = 0; Iteration < MaxIterations; ++Iteration)
|
||||||
{
|
{
|
||||||
const FVector Target = CurrentLocation + RemainingDelta;
|
const FVector Target = CurrentLocation + RemainingDelta;
|
||||||
const FTengriSweepResult Sweep = PerformSweep(WorldContext, CurrentLocation, Target, Capsule);
|
const FTengriSweepResult Sweep = PerformSweep(WorldContext, CurrentLocation, Target, Capsule, bShowDebug);
|
||||||
|
|
||||||
FinalResult.CollisionCount++;
|
FinalResult.CollisionCount++;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
// Request Games © All rights reserved
|
// Source/TengriPlatformer/Movement/Collision/TengriCollisionResolver.h
|
||||||
|
|
||||||
// Source/TengriPlatformer/Movement/Collision/TengriCollisionResolver.h
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|
@ -35,6 +33,7 @@ public:
|
||||||
* @param Thresholds - Surface classification thresholds
|
* @param Thresholds - Surface classification thresholds
|
||||||
* @param MaxStepHeight - Maximum step-up height
|
* @param MaxStepHeight - Maximum step-up height
|
||||||
* @param MaxIterations - Maximum slide iterations
|
* @param MaxIterations - Maximum slide iterations
|
||||||
|
* @param bShowDebug - Enable debug visualization
|
||||||
* @return Final position and collision info
|
* @return Final position and collision info
|
||||||
*/
|
*/
|
||||||
static FTengriSweepResult ResolveMovement(
|
static FTengriSweepResult ResolveMovement(
|
||||||
|
|
@ -44,7 +43,8 @@ public:
|
||||||
const UCapsuleComponent* Capsule,
|
const UCapsuleComponent* Capsule,
|
||||||
const FSurfaceThresholds& Thresholds,
|
const FSurfaceThresholds& Thresholds,
|
||||||
float MaxStepHeight,
|
float MaxStepHeight,
|
||||||
int32 MaxIterations
|
int32 MaxIterations,
|
||||||
|
bool bShowDebug = false
|
||||||
);
|
);
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
|
@ -56,7 +56,8 @@ public:
|
||||||
const UObject* WorldContext,
|
const UObject* WorldContext,
|
||||||
const FVector& Start,
|
const FVector& Start,
|
||||||
const FVector& End,
|
const FVector& End,
|
||||||
const UCapsuleComponent* Capsule
|
const UCapsuleComponent* Capsule,
|
||||||
|
bool bShowDebug = false
|
||||||
);
|
);
|
||||||
|
|
||||||
/** Attempt to step up over an obstacle */
|
/** Attempt to step up over an obstacle */
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
// Request Games © All rights reserved
|
// Source/TengriPlatformer/Movement/Collision/TengriSweepResult.h
|
||||||
|
|
||||||
// Source/TengriPlatformer/Movement/Collision/TengriSweepResult.h
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
// Request Games © All rights reserved
|
// Source/TengriPlatformer/Movement/Core/TengriMovementConfig.cpp
|
||||||
|
|
||||||
// Source/TengriPlatformer/Movement/Core/TengriMovementConfig.cpp
|
|
||||||
|
|
||||||
#include "TengriMovementConfig.h"
|
#include "TengriMovementConfig.h"
|
||||||
|
|
||||||
|
|
@ -18,36 +16,12 @@ void UTengriMovementConfig::PostEditChangeProperty(FPropertyChangedEvent& Proper
|
||||||
{
|
{
|
||||||
Super::PostEditChangeProperty(PropertyChangedEvent);
|
Super::PostEditChangeProperty(PropertyChangedEvent);
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════════════════
|
|
||||||
// 1. CALCULATE JUMP PHYSICS
|
|
||||||
// ════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
// Safety check to prevent division by zero
|
|
||||||
const float ClampedTime = FMath::Max(TimeToJumpApex, 0.1f);
|
|
||||||
|
|
||||||
// Calculate Gravity: g = (2 * h) / t^2
|
|
||||||
Gravity = (2.0f * MaxJumpHeight) / FMath::Square(ClampedTime);
|
|
||||||
|
|
||||||
// Calculate Initial Velocity: v = g * t
|
|
||||||
JumpVelocity = FMath::Abs(Gravity) * ClampedTime;
|
|
||||||
|
|
||||||
// Calculate Min Jump Velocity: v_min = sqrt(2 * g * h_min)
|
|
||||||
// This is the velocity we clamp to when button is released early
|
|
||||||
MinJumpVelocity = FMath::Sqrt(2.0f * FMath::Abs(Gravity) * MinJumpHeight);
|
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════════════════
|
|
||||||
// 2. VALIDATION LOGGING
|
|
||||||
// ════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
bool bHasErrors = false;
|
|
||||||
|
|
||||||
// Validate angle hierarchy
|
// Validate angle hierarchy
|
||||||
if (WalkableAngleDeg >= SteepSlopeAngleDeg)
|
if (WalkableAngleDeg >= SteepSlopeAngleDeg)
|
||||||
{
|
{
|
||||||
UE_LOG(LogTemp, Warning,
|
UE_LOG(LogTemp, Warning,
|
||||||
TEXT("TengriMovementConfig: WalkableAngle (%.1f) should be less than SteepSlopeAngle (%.1f)"),
|
TEXT("TengriMovementConfig: WalkableAngle (%.1f) should be less than SteepSlopeAngle (%.1f)"),
|
||||||
WalkableAngleDeg, SteepSlopeAngleDeg);
|
WalkableAngleDeg, SteepSlopeAngleDeg);
|
||||||
bHasErrors = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SteepSlopeAngleDeg >= WallAngleDeg)
|
if (SteepSlopeAngleDeg >= WallAngleDeg)
|
||||||
|
|
@ -55,63 +29,18 @@ void UTengriMovementConfig::PostEditChangeProperty(FPropertyChangedEvent& Proper
|
||||||
UE_LOG(LogTemp, Warning,
|
UE_LOG(LogTemp, Warning,
|
||||||
TEXT("TengriMovementConfig: SteepSlopeAngle (%.1f) should be less than WallAngle (%.1f)"),
|
TEXT("TengriMovementConfig: SteepSlopeAngle (%.1f) should be less than WallAngle (%.1f)"),
|
||||||
SteepSlopeAngleDeg, WallAngleDeg);
|
SteepSlopeAngleDeg, WallAngleDeg);
|
||||||
bHasErrors = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate collision parameters
|
|
||||||
if (MaxStepHeight <= 0.f)
|
if (MaxStepHeight <= 0.f)
|
||||||
{
|
{
|
||||||
UE_LOG(LogTemp, Warning,
|
UE_LOG(LogTemp, Warning,
|
||||||
TEXT("TengriMovementConfig: MaxStepHeight should be positive"));
|
TEXT("TengriMovementConfig: MaxStepHeight should be positive"));
|
||||||
bHasErrors = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (GroundSnapDistance <= 0.f)
|
if (GroundSnapDistance <= 0.f)
|
||||||
{
|
{
|
||||||
UE_LOG(LogTemp, Warning,
|
UE_LOG(LogTemp, Warning,
|
||||||
TEXT("TengriMovementConfig: GroundSnapDistance should be positive"));
|
TEXT("TengriMovementConfig: GroundSnapDistance should be positive"));
|
||||||
bHasErrors = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate jump parameters
|
|
||||||
if (MinJumpHeight >= MaxJumpHeight)
|
|
||||||
{
|
|
||||||
UE_LOG(LogTemp, Warning,
|
|
||||||
TEXT("TengriMovementConfig: MinJumpHeight (%.1f) should be less than MaxJumpHeight (%.1f)"),
|
|
||||||
MinJumpHeight, MaxJumpHeight);
|
|
||||||
bHasErrors = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (TimeToJumpApex <= 0.f)
|
|
||||||
{
|
|
||||||
UE_LOG(LogTemp, Warning,
|
|
||||||
TEXT("TengriMovementConfig: TimeToJumpApex should be positive"));
|
|
||||||
bHasErrors = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate air physics
|
|
||||||
if (AirControl < 0.f || AirControl > 1.f)
|
|
||||||
{
|
|
||||||
UE_LOG(LogTemp, Warning,
|
|
||||||
TEXT("TengriMovementConfig: AirControl should be between 0 and 1 (current: %.2f)"),
|
|
||||||
AirControl);
|
|
||||||
bHasErrors = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (FallingGravityScale < 1.f)
|
|
||||||
{
|
|
||||||
UE_LOG(LogTemp, Warning,
|
|
||||||
TEXT("TengriMovementConfig: FallingGravityScale should be >= 1.0 (current: %.2f)"),
|
|
||||||
FallingGravityScale);
|
|
||||||
bHasErrors = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log calculated values for verification
|
|
||||||
if (!bHasErrors)
|
|
||||||
{
|
|
||||||
UE_LOG(LogTemp, Log,
|
|
||||||
TEXT("TengriMovementConfig: Calculated Physics - Gravity: %.1f, JumpVel: %.1f, MinJumpVel: %.1f"),
|
|
||||||
Gravity, JumpVelocity, MinJumpVelocity);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
// Request Games © All rights reserved
|
// Source/TengriPlatformer/Movement/Core/TengriMovementConfig.h
|
||||||
|
|
||||||
// Source/TengriPlatformer/Movement/Core/TengriMovementConfig.h
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|
@ -83,6 +81,9 @@ public:
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Physics")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Physics")
|
||||||
float Friction = 8.0f;
|
float Friction = 8.0f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Physics")
|
||||||
|
float Gravity = 980.0f;
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Physics")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Physics")
|
||||||
float RotationSpeed = 360.0f;
|
float RotationSpeed = 360.0f;
|
||||||
|
|
||||||
|
|
@ -90,78 +91,6 @@ public:
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Physics")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Physics")
|
||||||
float MinSpeedForRotation = 10.0f;
|
float MinSpeedForRotation = 10.0f;
|
||||||
|
|
||||||
// ========================================================================
|
|
||||||
// JUMP CONFIGURATION (NEW)
|
|
||||||
// ========================================================================
|
|
||||||
|
|
||||||
/** Target height of the jump in cm (UE units) */
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Jump", meta = (ClampMin = "10.0"))
|
|
||||||
float MaxJumpHeight = 200.0f;
|
|
||||||
|
|
||||||
/** Minimum height for a short hop (when button is released early) */
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Jump", meta = (ClampMin = "1.0"))
|
|
||||||
float MinJumpHeight = 40.0f;
|
|
||||||
|
|
||||||
/** Time (seconds) to reach the peak of the jump. Defines "heaviness". */
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Jump", meta = (ClampMin = "0.1", ClampMax = "2.0"))
|
|
||||||
float TimeToJumpApex = 0.5f;
|
|
||||||
|
|
||||||
// ========================================================================
|
|
||||||
// JUMP FEEL (TIMINGS)
|
|
||||||
// ========================================================================
|
|
||||||
|
|
||||||
/** Time (seconds) after falling off a ledge during which jump is still allowed */
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Jump|Feel", meta = (ClampMin = "0.0"))
|
|
||||||
float CoyoteTime = 0.15f;
|
|
||||||
|
|
||||||
/** Time (seconds) to buffer a jump input before hitting the ground */
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Jump|Feel", meta = (ClampMin = "0.0"))
|
|
||||||
float JumpBufferTime = 0.15f;
|
|
||||||
|
|
||||||
// ========================================================================
|
|
||||||
// AIR PHYSICS
|
|
||||||
// ========================================================================
|
|
||||||
|
|
||||||
/** Multiplier for acceleration when in air (0 = no control, 1 = full control) */
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Air Physics", meta = (ClampMin = "0.0", ClampMax = "1.0"))
|
|
||||||
float AirControl = 0.5f;
|
|
||||||
|
|
||||||
/** Friction applied while in air (usually 0 for platformers) */
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Air Physics")
|
|
||||||
float AirFriction = 0.0f;
|
|
||||||
|
|
||||||
// ========================================================================
|
|
||||||
// CALCULATED VALUES (READ ONLY)
|
|
||||||
// ========================================================================
|
|
||||||
|
|
||||||
/** Gravity magnitude calculated from Jump Height & Time */
|
|
||||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Calculated")
|
|
||||||
float Gravity = 980.0f;
|
|
||||||
|
|
||||||
/** Initial Z velocity required to reach MaxJumpHeight */
|
|
||||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Calculated")
|
|
||||||
float JumpVelocity = 0.0f;
|
|
||||||
|
|
||||||
/** Velocity cut-off for variable jump height */
|
|
||||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Calculated")
|
|
||||||
float MinJumpVelocity = 0.0f;
|
|
||||||
|
|
||||||
// ========================================================================
|
|
||||||
// AIR PHYSICS (NEW)
|
|
||||||
// ========================================================================
|
|
||||||
|
|
||||||
/** Multiplier for gravity when falling. Makes jump feel "heavy" and snappy. */
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Air Physics", meta = (ClampMin = "1.0"))
|
|
||||||
float FallingGravityScale = 1.5f;
|
|
||||||
|
|
||||||
/** Maximum falling speed (cm/s). Prevents infinite acceleration. */
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Air Physics", meta = (ClampMin = "0.0"))
|
|
||||||
float TerminalVelocity = 2000.0f;
|
|
||||||
|
|
||||||
/** Z velocity threshold to consider a landing "heavy" (e.g. for landing animation/shake) */
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Air Physics")
|
|
||||||
float HeavyLandVelocityThreshold = -1000.0f;
|
|
||||||
|
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
// SURFACE ANGLES
|
// SURFACE ANGLES
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Request Games © All rights reserved
|
|
||||||
|
|
||||||
// Source/TengriPlatformer/Movement/TengriMovementComponent.cpp
|
// Source/TengriPlatformer/Movement/TengriMovementComponent.cpp
|
||||||
|
|
||||||
#include "TengriMovementComponent.h"
|
#include "TengriMovementComponent.h"
|
||||||
|
|
@ -22,20 +20,6 @@ namespace TengriMovement
|
||||||
constexpr float JumpingThreshold = 10.0f; // cm/s, skip snap when moving up
|
constexpr float JumpingThreshold = 10.0f; // cm/s, skip snap when moving up
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// PENDING EVENTS (NEW)
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Structure to accumulate events during physics loop.
|
|
||||||
* These are broadcasted AFTER all physics iterations complete.
|
|
||||||
*/
|
|
||||||
struct FPendingLandingEvent
|
|
||||||
{
|
|
||||||
bool bIsHeavy;
|
|
||||||
float LandingVelocityZ;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// CONSTRUCTOR
|
// CONSTRUCTOR
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
@ -59,7 +43,7 @@ void UTengriMovementComponent::BeginPlay()
|
||||||
|
|
||||||
void UTengriMovementComponent::InitializeSystem()
|
void UTengriMovementComponent::InitializeSystem()
|
||||||
{
|
{
|
||||||
const AActor* Owner = GetOwner();
|
AActor* Owner = GetOwner();
|
||||||
if (!Owner)
|
if (!Owner)
|
||||||
{
|
{
|
||||||
UE_LOG(LogTengriMovement, Error, TEXT("InitializeSystem failed: No owner"));
|
UE_LOG(LogTengriMovement, Error, TEXT("InitializeSystem failed: No owner"));
|
||||||
|
|
@ -113,32 +97,19 @@ void UTengriMovementComponent::InitializeSystem()
|
||||||
// BLUEPRINT API
|
// BLUEPRINT API
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
void UTengriMovementComponent::SetInputVector(const FVector NewInput)
|
void UTengriMovementComponent::SetInputVector(FVector NewInput)
|
||||||
{
|
{
|
||||||
InputVector = NewInput.GetClampedToMaxSize(1.0f);
|
InputVector = NewInput.GetClampedToMaxSize(1.0f);
|
||||||
InputVector.Z = 0.0f;
|
InputVector.Z = 0.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
void UTengriMovementComponent::SetJumpInput(const bool bPressed)
|
|
||||||
{
|
|
||||||
if (bPressed && !bIsJumpHeld)
|
|
||||||
{
|
|
||||||
if (MovementConfig)
|
|
||||||
{
|
|
||||||
JumpBufferTimer = MovementConfig->JumpBufferTime;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bIsJumpHeld = bPressed;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TICK
|
// TICK
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
void UTengriMovementComponent::TickComponent(
|
void UTengriMovementComponent::TickComponent(
|
||||||
const float DeltaTime,
|
float DeltaTime,
|
||||||
const ELevelTick TickType,
|
ELevelTick TickType,
|
||||||
FActorComponentTickFunction* ThisTickFunction)
|
FActorComponentTickFunction* ThisTickFunction)
|
||||||
{
|
{
|
||||||
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
|
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
|
||||||
|
|
@ -163,12 +134,6 @@ void UTengriMovementComponent::TickComponent(
|
||||||
TimeAccumulator = MaxAccumulatorTime;
|
TimeAccumulator = MaxAccumulatorTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════════════
|
|
||||||
// PENDING EVENTS (Accumulated during physics)
|
|
||||||
// ════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
TArray<FPendingLandingEvent> PendingLandings;
|
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════
|
||||||
// DETERMINISTIC PHYSICS LOOP
|
// DETERMINISTIC PHYSICS LOOP
|
||||||
// ════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
|
@ -181,8 +146,7 @@ void UTengriMovementComponent::TickComponent(
|
||||||
SavePreviousPhysicsState();
|
SavePreviousPhysicsState();
|
||||||
|
|
||||||
// Run deterministic physics at fixed rate
|
// Run deterministic physics at fixed rate
|
||||||
// Pass reference to PendingLandings to accumulate events
|
TickPhysics(FixedTimeStep);
|
||||||
TickPhysics(FixedTimeStep, PendingLandings);
|
|
||||||
|
|
||||||
// Consume fixed time from accumulator
|
// Consume fixed time from accumulator
|
||||||
TimeAccumulator -= FixedTimeStep;
|
TimeAccumulator -= FixedTimeStep;
|
||||||
|
|
@ -199,28 +163,6 @@ void UTengriMovementComponent::TickComponent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════════════
|
|
||||||
// BROADCAST ACCUMULATED EVENTS
|
|
||||||
// ════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
// Only broadcast the LAST landing event if multiple occurred
|
|
||||||
// (Prevents spam if character lands multiple times in one frame)
|
|
||||||
if (PendingLandings.Num() > 0)
|
|
||||||
{
|
|
||||||
const FPendingLandingEvent& LastLanding = PendingLandings.Last();
|
|
||||||
if (OnLanded.IsBound())
|
|
||||||
{
|
|
||||||
OnLanded.Broadcast(LastLanding.bIsHeavy);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (LastLanding.bIsHeavy)
|
|
||||||
{
|
|
||||||
UE_LOG(LogTengriMovement, Verbose,
|
|
||||||
TEXT("Heavy landing detected! Velocity: %.1f cm/s"),
|
|
||||||
LastLanding.LandingVelocityZ);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════
|
||||||
// INTERPOLATION & RENDERING
|
// INTERPOLATION & RENDERING
|
||||||
// ════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
|
@ -249,143 +191,47 @@ void UTengriMovementComponent::TickComponent(
|
||||||
// PHYSICS TICK
|
// PHYSICS TICK
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
void UTengriMovementComponent::TickPhysics(
|
void UTengriMovementComponent::TickPhysics(float FixedDeltaTime)
|
||||||
const float FixedDeltaTime,
|
|
||||||
TArray<FPendingLandingEvent>& OutPendingLandings)
|
|
||||||
{
|
{
|
||||||
// ════════════════════════════════════════════════════════════════════
|
|
||||||
// Phase 0: State & Timer Updates
|
|
||||||
// ════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
// 1. Manage Coyote Time (Can we jump while falling?)
|
|
||||||
if (bIsGrounded)
|
|
||||||
{
|
|
||||||
CoyoteTimer = MovementConfig->CoyoteTime;
|
|
||||||
bHasJumpedThisFrame = false;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
CoyoteTimer -= FixedDeltaTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Manage Jump Buffer (Did we press jump recently?)
|
|
||||||
if (JumpBufferTimer > 0.0f)
|
|
||||||
{
|
|
||||||
JumpBufferTimer -= FixedDeltaTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════════════
|
|
||||||
// Phase 1: Jump Execution
|
|
||||||
// ════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
// Check if we can jump:
|
|
||||||
// 1. Button was pressed recently (Buffer > 0)
|
|
||||||
// 2. We are on ground OR recently left ground (Coyote > 0)
|
|
||||||
// 3. We haven't already jumped this frame (double jump prevention)
|
|
||||||
if (JumpBufferTimer > 0.0f && CoyoteTimer > 0.0f && !bHasJumpedThisFrame)
|
|
||||||
{
|
|
||||||
// Apply Jump Velocity
|
|
||||||
PhysicsVelocity.Z = MovementConfig->JumpVelocity;
|
|
||||||
|
|
||||||
// Update State
|
|
||||||
bIsGrounded = false;
|
|
||||||
bHasJumpedThisFrame = true;
|
|
||||||
|
|
||||||
// Consume Timers
|
|
||||||
JumpBufferTimer = 0.0f;
|
|
||||||
CoyoteTimer = 0.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════════════
|
|
||||||
// Phase 2: Variable Jump Height
|
|
||||||
// ════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
// If moving up AND button released -> Cut velocity
|
|
||||||
if (PhysicsVelocity.Z > MovementConfig->MinJumpVelocity && !bIsJumpHeld)
|
|
||||||
{
|
|
||||||
PhysicsVelocity.Z = MovementConfig->MinJumpVelocity;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════════════
|
|
||||||
// Phase 3: Horizontal Movement (Air Control)
|
|
||||||
// ════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
const float CurrentZ = PhysicsVelocity.Z;
|
|
||||||
FVector HorizontalVelocity(PhysicsVelocity.X, PhysicsVelocity.Y, 0.f);
|
|
||||||
|
|
||||||
// Select Acceleration/Friction based on state
|
|
||||||
const float CurrentAccel = bIsGrounded
|
|
||||||
? MovementConfig->Acceleration
|
|
||||||
: (MovementConfig->Acceleration * MovementConfig->AirControl);
|
|
||||||
|
|
||||||
const float CurrentFriction = bIsGrounded
|
|
||||||
? MovementConfig->Friction
|
|
||||||
: MovementConfig->AirFriction;
|
|
||||||
|
|
||||||
if (!InputVector.IsNearlyZero())
|
|
||||||
{
|
|
||||||
const FVector TargetVelocity = InputVector * MovementConfig->MaxSpeed;
|
|
||||||
HorizontalVelocity = FMath::VInterpTo(
|
|
||||||
HorizontalVelocity,
|
|
||||||
TargetVelocity,
|
|
||||||
FixedDeltaTime,
|
|
||||||
CurrentAccel // <-- Uses Air Control if flying
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
HorizontalVelocity = FMath::VInterpTo(
|
|
||||||
HorizontalVelocity,
|
|
||||||
FVector::ZeroVector,
|
|
||||||
FixedDeltaTime,
|
|
||||||
CurrentFriction // <-- Usually 0 in air
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
PhysicsVelocity = HorizontalVelocity;
|
|
||||||
PhysicsVelocity.Z = CurrentZ;
|
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════
|
||||||
// Phase 4: Rotation
|
// Phase 1: Acceleration & Friction
|
||||||
// ════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
FRotator TargetRot = PhysicsRotation; // Default: maintain current rotation
|
const float CurrentZ = PhysicsVelocity.Z;
|
||||||
bool bShouldUpdateRotation = false;
|
FVector HorizontalVelocity(PhysicsVelocity.X, PhysicsVelocity.Y, 0.f);
|
||||||
float CurrentRotSpeed = MovementConfig->RotationSpeed;
|
|
||||||
|
|
||||||
// SCENARIO A: STRAFE MODE (Aiming)
|
if (!InputVector.IsNearlyZero())
|
||||||
// Character rotates to match camera even when stationary.
|
|
||||||
// Used for combat/precise aiming scenarios.
|
|
||||||
if (bStrafing)
|
|
||||||
{
|
{
|
||||||
if (const APawn* PawnOwner = Cast<APawn>(GetOwner()))
|
const FVector TargetVelocity = InputVector * MovementConfig->MaxSpeed;
|
||||||
{
|
HorizontalVelocity = FMath::VInterpTo(
|
||||||
if (const AController* C = PawnOwner->GetController())
|
HorizontalVelocity,
|
||||||
{
|
TargetVelocity,
|
||||||
TargetRot = C->GetControlRotation();
|
FixedDeltaTime,
|
||||||
bShouldUpdateRotation = true;
|
MovementConfig->Acceleration
|
||||||
|
);
|
||||||
// Optional: Faster rotation in combat for responsive feel
|
|
||||||
CurrentRotSpeed *= 2.0f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// SCENARIO B: STANDARD MOVEMENT (Classic platformer)
|
else
|
||||||
// Character rotates toward velocity direction only when moving.
|
|
||||||
// Maintains forward orientation based on movement.
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
if (const float MinSpeedSq = FMath::Square(MovementConfig->MinSpeedForRotation); PhysicsVelocity.SizeSquared2D() > MinSpeedSq)
|
HorizontalVelocity = FMath::VInterpTo(
|
||||||
{
|
HorizontalVelocity,
|
||||||
TargetRot = PhysicsVelocity.ToOrientationRotator();
|
FVector::ZeroVector,
|
||||||
bShouldUpdateRotation = true;
|
FixedDeltaTime,
|
||||||
}
|
MovementConfig->Friction
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// APPLY ROTATION
|
PhysicsVelocity = HorizontalVelocity;
|
||||||
if (bShouldUpdateRotation)
|
PhysicsVelocity.Z = CurrentZ;
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
// Phase 2: Rotation
|
||||||
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
const float MinSpeedSq = FMath::Square(MovementConfig->MinSpeedForRotation);
|
||||||
|
|
||||||
|
if (PhysicsVelocity.SizeSquared2D() > MinSpeedSq)
|
||||||
{
|
{
|
||||||
// Always prevent pitch/roll to keep character upright
|
FRotator TargetRot = PhysicsVelocity.ToOrientationRotator();
|
||||||
TargetRot.Pitch = 0.0f;
|
TargetRot.Pitch = 0.0f;
|
||||||
TargetRot.Roll = 0.0f;
|
TargetRot.Roll = 0.0f;
|
||||||
|
|
||||||
|
|
@ -393,112 +239,73 @@ void UTengriMovementComponent::TickPhysics(
|
||||||
PhysicsRotation,
|
PhysicsRotation,
|
||||||
TargetRot,
|
TargetRot,
|
||||||
FixedDeltaTime,
|
FixedDeltaTime,
|
||||||
CurrentRotSpeed
|
MovementConfig->RotationSpeed
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════
|
||||||
// Phase 5: Gravity
|
// Phase 3: Gravity
|
||||||
// ════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
if (!bIsGrounded)
|
if (!bIsGrounded)
|
||||||
{
|
{
|
||||||
float CurrentGravity = MovementConfig->Gravity;
|
PhysicsVelocity.Z -= MovementConfig->Gravity * FixedDeltaTime;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PhysicsVelocity.Z = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
// Apply extra gravity if falling (makes jump snappy)
|
// ════════════════════════════════════════════════════════════════════
|
||||||
if (PhysicsVelocity.Z < 0.0f)
|
// Phase 4: Collision Resolution
|
||||||
{
|
// ════════════════════════════════════════════════════════════════════
|
||||||
CurrentGravity *= MovementConfig->FallingGravityScale;
|
|
||||||
}
|
|
||||||
|
|
||||||
PhysicsVelocity.Z -= CurrentGravity * FixedDeltaTime;
|
const FVector DesiredDelta = PhysicsVelocity * FixedDeltaTime;
|
||||||
|
|
||||||
// Clamp to terminal velocity
|
const FTengriSweepResult MoveResult = UTengriCollisionResolver::ResolveMovement(
|
||||||
if (PhysicsVelocity.Z < -MovementConfig->TerminalVelocity)
|
this,
|
||||||
{
|
PhysicsLocation,
|
||||||
PhysicsVelocity.Z = -MovementConfig->TerminalVelocity;
|
DesiredDelta,
|
||||||
}
|
OwnerCapsule,
|
||||||
}
|
CachedThresholds,
|
||||||
|
MovementConfig->MaxStepHeight,
|
||||||
|
MovementConfig->MaxSlideIterations,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════════════
|
PhysicsLocation = MoveResult.Location;
|
||||||
// Phase 6: Collision Resolution
|
|
||||||
// ════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
const FVector DesiredDelta = PhysicsVelocity * FixedDeltaTime;
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
// Phase 5: Ground Snapping
|
||||||
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
const FTengriSweepResult MoveResult = UTengriCollisionResolver::ResolveMovement(
|
FHitResult SnapHit;
|
||||||
this,
|
const bool bJustSnapped = PerformGroundSnapping(PhysicsLocation, SnapHit);
|
||||||
PhysicsLocation,
|
|
||||||
DesiredDelta,
|
|
||||||
OwnerCapsule,
|
|
||||||
CachedThresholds,
|
|
||||||
MovementConfig->MaxStepHeight,
|
|
||||||
MovementConfig->MaxSlideIterations
|
|
||||||
);
|
|
||||||
|
|
||||||
PhysicsLocation = MoveResult.Location;
|
if (bJustSnapped && !InputVector.IsNearlyZero())
|
||||||
|
{
|
||||||
|
// Preserve momentum along slope
|
||||||
|
PhysicsVelocity = UTengriCollisionResolver::ProjectVelocity(
|
||||||
|
PhysicsVelocity,
|
||||||
|
SnapHit.ImpactNormal
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════
|
||||||
// Phase 7: Ground Snapping
|
// Phase 6: State Update
|
||||||
// ════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
bool bJustSnapped = false;
|
// Determine grounded state from snap or collision
|
||||||
|
const bool bHitWalkable = MoveResult.bBlocked &&
|
||||||
|
CachedThresholds.IsWalkable(MoveResult.Hit.ImpactNormal.Z);
|
||||||
|
|
||||||
// Only snap if we are NOT jumping upwards
|
bIsGrounded = bJustSnapped || bHitWalkable;
|
||||||
if (PhysicsVelocity.Z <= 0.0f)
|
|
||||||
{
|
|
||||||
FHitResult SnapHit;
|
|
||||||
bJustSnapped = PerformGroundSnapping(PhysicsLocation, SnapHit);
|
|
||||||
|
|
||||||
// Project velocity onto slope if snapped
|
|
||||||
if (bJustSnapped && !InputVector.IsNearlyZero())
|
|
||||||
{
|
|
||||||
PhysicsVelocity = UTengriCollisionResolver::ProjectVelocity(
|
|
||||||
PhysicsVelocity,
|
|
||||||
SnapHit.ImpactNormal
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════════════
|
// Prevent Z velocity accumulation when grounded
|
||||||
// Phase 8: State Update
|
if (bIsGrounded && PhysicsVelocity.Z < 0.f)
|
||||||
// ════════════════════════════════════════════════════════════════════
|
{
|
||||||
|
PhysicsVelocity.Z = 0.f;
|
||||||
const bool bWasGrounded = bIsGrounded;
|
}
|
||||||
|
|
||||||
// We are grounded if:
|
|
||||||
// 1. We snapped to ground, OR
|
|
||||||
// 2. We hit a walkable surface during movement
|
|
||||||
const bool bHitWalkable = MoveResult.bBlocked &&
|
|
||||||
CachedThresholds.IsWalkable(MoveResult.Hit.ImpactNormal.Z);
|
|
||||||
|
|
||||||
const bool bNowGrounded = bJustSnapped || bHitWalkable;
|
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════════════
|
|
||||||
// Phase 9: Landing Detection
|
|
||||||
// ════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
if (!bWasGrounded && bNowGrounded)
|
|
||||||
{
|
|
||||||
// Store landing velocity BEFORE we zero it
|
|
||||||
const float LandingVelocityZ = PhysicsVelocity.Z;
|
|
||||||
const bool bIsHeavy = LandingVelocityZ < MovementConfig->HeavyLandVelocityThreshold;
|
|
||||||
|
|
||||||
// Accumulate event instead of broadcasting immediately
|
|
||||||
FPendingLandingEvent LandingEvent;
|
|
||||||
LandingEvent.bIsHeavy = bIsHeavy;
|
|
||||||
LandingEvent.LandingVelocityZ = LandingVelocityZ;
|
|
||||||
OutPendingLandings.Add(LandingEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update grounded state
|
|
||||||
bIsGrounded = bNowGrounded;
|
|
||||||
|
|
||||||
// Reset Z velocity if we landed or are on ground
|
|
||||||
if (bIsGrounded && PhysicsVelocity.Z < 0.f)
|
|
||||||
{
|
|
||||||
PhysicsVelocity.Z = 0.f;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
@ -555,7 +362,8 @@ bool UTengriMovementComponent::PerformGroundSnapping(
|
||||||
this,
|
this,
|
||||||
Start,
|
Start,
|
||||||
End,
|
End,
|
||||||
OwnerCapsule
|
OwnerCapsule,
|
||||||
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
if (Sweep.bBlocked && CachedThresholds.IsWalkable(Sweep.Hit.ImpactNormal.Z))
|
if (Sweep.bBlocked && CachedThresholds.IsWalkable(Sweep.Hit.ImpactNormal.Z))
|
||||||
|
|
@ -567,12 +375,4 @@ bool UTengriMovementComponent::PerformGroundSnapping(
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
|
|
||||||
void UTengriMovementComponent::ForceRotation(const FRotator& NewRotation)
|
|
||||||
{
|
|
||||||
// Обновляем все внутренние состояния, чтобы физика "подхватила" новый поворот
|
|
||||||
PhysicsRotation = NewRotation;
|
|
||||||
RenderRotation = NewRotation;
|
|
||||||
PreviousPhysicsRotation = NewRotation; // Сбрасываем интерполяцию
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Request Games © All rights reserved
|
|
||||||
|
|
||||||
// Source/TengriPlatformer/Movement/TengriMovementComponent.h
|
// Source/TengriPlatformer/Movement/TengriMovementComponent.h
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
@ -10,9 +8,6 @@
|
||||||
#include "TengriMovementComponent.generated.h"
|
#include "TengriMovementComponent.generated.h"
|
||||||
|
|
||||||
class UCapsuleComponent;
|
class UCapsuleComponent;
|
||||||
struct FPendingLandingEvent; // Forward declaration
|
|
||||||
|
|
||||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnTengriLandingSignature, bool, bIsHeavy);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom movement component for deterministic 3D platformer physics.
|
* Custom movement component for deterministic 3D platformer physics.
|
||||||
|
|
@ -22,7 +17,6 @@ DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnTengriLandingSignature, bool, bIs
|
||||||
* - Physics State: Updated at fixed rate (default 120Hz) for determinism
|
* - Physics State: Updated at fixed rate (default 120Hz) for determinism
|
||||||
* - Render State: Interpolated between physics states for smooth visuals
|
* - Render State: Interpolated between physics states for smooth visuals
|
||||||
* - Accumulator: Manages variable frame delta accumulation
|
* - Accumulator: Manages variable frame delta accumulation
|
||||||
* - Event Accumulation: Events during physics loop are broadcast after completion
|
|
||||||
*/
|
*/
|
||||||
UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
|
UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
|
||||||
class TENGRIPLATFORMER_API UTengriMovementComponent : public UActorComponent
|
class TENGRIPLATFORMER_API UTengriMovementComponent : public UActorComponent
|
||||||
|
|
@ -32,22 +26,9 @@ class TENGRIPLATFORMER_API UTengriMovementComponent : public UActorComponent
|
||||||
public:
|
public:
|
||||||
UTengriMovementComponent();
|
UTengriMovementComponent();
|
||||||
|
|
||||||
/** Event triggered when character lands (broadcast after physics loop completes) */
|
|
||||||
UPROPERTY(BlueprintAssignable, Category = "Tengri Movement|Events")
|
|
||||||
FOnTengriLandingSignature OnLanded;
|
|
||||||
|
|
||||||
/** Instantly snaps character rotation to a new value (bypassing interpolation) */
|
|
||||||
void ForceRotation(const FRotator& NewRotation);
|
|
||||||
|
|
||||||
/** Enable strafe mode (character rotates toward camera instead of movement direction) */
|
|
||||||
void SetStrafing(const bool bEnabled) { bStrafing = bEnabled; }
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual void BeginPlay() override;
|
virtual void BeginPlay() override;
|
||||||
|
|
||||||
/** Strafe mode flag */
|
|
||||||
bool bStrafing = false;
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
virtual void TickComponent(float DeltaTime, ELevelTick TickType,
|
virtual void TickComponent(float DeltaTime, ELevelTick TickType,
|
||||||
FActorComponentTickFunction* ThisTickFunction) override;
|
FActorComponentTickFunction* ThisTickFunction) override;
|
||||||
|
|
@ -63,13 +44,6 @@ public:
|
||||||
UFUNCTION(BlueprintCallable, Category = "Tengri Movement")
|
UFUNCTION(BlueprintCallable, Category = "Tengri Movement")
|
||||||
void SetInputVector(FVector NewInput);
|
void SetInputVector(FVector NewInput);
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates jump input state. Call from PlayerController.
|
|
||||||
* @param bPressed - True if button just pressed or currently held
|
|
||||||
*/
|
|
||||||
UFUNCTION(BlueprintCallable, Category = "Tengri Movement")
|
|
||||||
void SetJumpInput(bool bPressed);
|
|
||||||
|
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
// CONFIGURATION
|
// CONFIGURATION
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
@ -151,22 +125,6 @@ private:
|
||||||
/** Normalized input vector (Z always 0) */
|
/** Normalized input vector (Z always 0) */
|
||||||
FVector InputVector = FVector::ZeroVector;
|
FVector InputVector = FVector::ZeroVector;
|
||||||
|
|
||||||
// ========================================================================
|
|
||||||
// JUMP STATE (Internal)
|
|
||||||
// ========================================================================
|
|
||||||
|
|
||||||
/** Timer for Jump Buffering. > 0 means jump was recently pressed. */
|
|
||||||
float JumpBufferTimer = 0.0f;
|
|
||||||
|
|
||||||
/** Timer for Coyote Time. > 0 means we can still jump even if in air. */
|
|
||||||
float CoyoteTimer = 0.0f;
|
|
||||||
|
|
||||||
/** True if jump button is currently held down (for variable jump height) */
|
|
||||||
bool bIsJumpHeld = false;
|
|
||||||
|
|
||||||
/** Flag to prevent Coyote Time reactivation immediately after jumping */
|
|
||||||
bool bHasJumpedThisFrame = false;
|
|
||||||
|
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
// INITIALIZATION
|
// INITIALIZATION
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
@ -174,16 +132,15 @@ private:
|
||||||
void InitializeSystem();
|
void InitializeSystem();
|
||||||
|
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
// PHYSICS TICK (UPDATED)
|
// PHYSICS TICK
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deterministic physics update at fixed timestep.
|
* Deterministic physics update at fixed timestep.
|
||||||
* All movement logic runs here with constant delta time.
|
* All movement logic runs here with constant delta time.
|
||||||
* @param FixedDeltaTime - Fixed timestep duration (e.g., 1/120 sec)
|
* @param FixedDeltaTime - Fixed timestep duration (e.g., 1/120 sec)
|
||||||
* @param OutPendingLandings - Accumulator for landing events (broadcast later)
|
|
||||||
*/
|
*/
|
||||||
void TickPhysics(float FixedDeltaTime, TArray<FPendingLandingEvent>& OutPendingLandings);
|
void TickPhysics(float FixedDeltaTime);
|
||||||
|
|
||||||
/** Save current physics state before next physics step */
|
/** Save current physics state before next physics step */
|
||||||
void SavePreviousPhysicsState();
|
void SavePreviousPhysicsState();
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,17 @@
|
||||||
// Source/TengriPlatformer/Movement/TengriMovementComponent.ts
|
// Source/TengriPlatformer/Movement/TengriMovementComponent.ts
|
||||||
|
|
||||||
import { ActorComponent } from '/Content/UE/ActorComponent.ts';
|
import { ActorComponent } from '/Content/UE/ActorComponent.ts';
|
||||||
import { Vector } from '/Content/UE/Vector.ts';
|
import type { Vector } from '/Content/UE/Vector.ts';
|
||||||
import type { DA_TengriMovementConfig } from '/Content/Movement/DA_TengriMovementConfig.ts';
|
import type { DA_TengriMovementConfig } from '/Content/Movement/DA_TengriMovementConfig.ts';
|
||||||
|
|
||||||
type ConstructorParams = {
|
|
||||||
MovementConfig: typeof DA_TengriMovementConfig;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class TengriMovementComponent extends ActorComponent {
|
export class TengriMovementComponent extends ActorComponent {
|
||||||
constructor(data?: ConstructorParams) {
|
constructor(MovementConfig: typeof DA_TengriMovementConfig) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
console.log(data);
|
console.log(MovementConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
public SetInputVector(NewInput: Vector = new Vector()): void {
|
public SetInputVector(NewInput: Vector): void {
|
||||||
console.log(NewInput);
|
console.log(NewInput);
|
||||||
}
|
}
|
||||||
|
|
||||||
public SetJumpInput(bPressed: boolean = false): void {
|
|
||||||
console.log(bPressed);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
// Request Games © All rights reserved
|
// Fill out your copyright notice in the Description page of Project Settings.
|
||||||
|
|
||||||
using UnrealBuildTool;
|
using UnrealBuildTool;
|
||||||
|
|
||||||
public class TengriPlatformer : ModuleRules
|
public class TengriPlatformer : ModuleRules
|
||||||
{
|
{
|
||||||
public TengriPlatformer(ReadOnlyTargetRules target) : base(target)
|
public TengriPlatformer(ReadOnlyTargetRules Target) : base(Target)
|
||||||
{
|
{
|
||||||
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||||
|
|
||||||
PublicDependencyModuleNames.AddRange(["Core", "CoreUObject", "Engine", "InputCore"]);
|
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore" });
|
||||||
|
|
||||||
PrivateDependencyModuleNames.AddRange(["EnhancedInput"]);
|
PrivateDependencyModuleNames.AddRange(new string[] { });
|
||||||
|
|
||||||
// Uncomment if you are using Slate UI
|
// Uncomment if you are using Slate UI
|
||||||
// PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
|
// PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// Request Games © All rights reserved
|
// Fill out your copyright notice in the Description page of Project Settings.
|
||||||
|
|
||||||
#include "TengriPlatformer.h"
|
#include "TengriPlatformer.h"
|
||||||
#include "Modules/ModuleManager.h"
|
#include "Modules/ModuleManager.h"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// Request Games © All rights reserved
|
// Fill out your copyright notice in the Description page of Project Settings.
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
// Request Games © All rights reserved
|
|
||||||
|
|
||||||
// Source/TengriPlatformer/World/TengriPickupActor.cpp
|
|
||||||
|
|
||||||
#include "TengriPickupActor.h"
|
|
||||||
#include "Components/StaticMeshComponent.h"
|
|
||||||
#include "Components/SphereComponent.h"
|
|
||||||
|
|
||||||
ATengriPickupActor::ATengriPickupActor()
|
|
||||||
{
|
|
||||||
PrimaryActorTick.bCanEverTick = false; // Физике тик не нужен
|
|
||||||
|
|
||||||
// 1. Mesh Setup
|
|
||||||
MeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MeshComp"));
|
|
||||||
RootComponent = MeshComp;
|
|
||||||
|
|
||||||
// Enable physics by default
|
|
||||||
MeshComp->SetSimulatePhysics(true);
|
|
||||||
MeshComp->SetCollisionProfileName(TEXT("PhysicsActor")); // Стандартный профиль UE для предметов
|
|
||||||
MeshComp->SetMassOverrideInKg(NAME_None, 10.0f); // Вес по дефолту
|
|
||||||
|
|
||||||
// 2. Trigger Setup
|
|
||||||
TriggerComp = CreateDefaultSubobject<USphereComponent>(TEXT("TriggerComp"));
|
|
||||||
TriggerComp->SetupAttachment(MeshComp);
|
|
||||||
TriggerComp->SetSphereRadius(80.0f);
|
|
||||||
|
|
||||||
// Trigger collision settings
|
|
||||||
TriggerComp->SetCollisionProfileName(TEXT("Trigger"));
|
|
||||||
}
|
|
||||||
|
|
||||||
void ATengriPickupActor::BeginPlay()
|
|
||||||
{
|
|
||||||
Super::BeginPlay();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ATengriPickupActor::OnPickedUp(USceneComponent* AttachTo, const FName SocketName)
|
|
||||||
{
|
|
||||||
if (!MeshComp || !AttachTo) return;
|
|
||||||
|
|
||||||
bIsHeld = true;
|
|
||||||
|
|
||||||
// 1. Disable Physics
|
|
||||||
MeshComp->SetSimulatePhysics(false);
|
|
||||||
|
|
||||||
// 2. Disable Collision
|
|
||||||
MeshComp->SetCollisionEnabled(ECollisionEnabled::NoCollision);
|
|
||||||
|
|
||||||
// 3. Attach to Hand
|
|
||||||
FAttachmentTransformRules AttachmentRules(
|
|
||||||
EAttachmentRule::SnapToTarget, // Location Rule
|
|
||||||
EAttachmentRule::SnapToTarget, // Rotation Rule
|
|
||||||
EAttachmentRule::KeepWorld, // Scale Rule
|
|
||||||
false // bWeldSimulatedBodies (добавили этот аргумент)
|
|
||||||
);
|
|
||||||
|
|
||||||
AttachToComponent(AttachTo, AttachmentRules, SocketName);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ATengriPickupActor::OnDropped(const FVector Impulse, const bool bVelChange)
|
|
||||||
{
|
|
||||||
if (!MeshComp) return;
|
|
||||||
|
|
||||||
bIsHeld = false;
|
|
||||||
|
|
||||||
// 1. Detach
|
|
||||||
DetachFromActor(FDetachmentTransformRules::KeepWorldTransform);
|
|
||||||
|
|
||||||
// 2. Re-enable Collision
|
|
||||||
MeshComp->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
|
|
||||||
|
|
||||||
if (!MeshComp) return;
|
|
||||||
|
|
||||||
// 3. Re-enable Physics
|
|
||||||
MeshComp->SetSimulatePhysics(true);
|
|
||||||
|
|
||||||
// 4. Apply Throw Force
|
|
||||||
if (!Impulse.IsNearlyZero())
|
|
||||||
{
|
|
||||||
MeshComp->AddImpulse(Impulse, NAME_None, bVelChange);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
// Request Games © All rights reserved
|
|
||||||
|
|
||||||
// Source/TengriPlatformer/World/TengriPickupActor.cpp
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
|
||||||
#include "GameFramework/Actor.h"
|
|
||||||
#include "TengriPickupActor.generated.h"
|
|
||||||
|
|
||||||
class USphereComponent;
|
|
||||||
class UStaticMeshComponent;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base class for all physics pickup items (rocks, bones, keys).
|
|
||||||
* Handles physics states when held/dropped.
|
|
||||||
*/
|
|
||||||
UCLASS()
|
|
||||||
class TENGRIPLATFORMER_API ATengriPickupActor : public AActor
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
ATengriPickupActor();
|
|
||||||
|
|
||||||
protected:
|
|
||||||
virtual void BeginPlay() override;
|
|
||||||
|
|
||||||
public:
|
|
||||||
// ========================================================================
|
|
||||||
// COMPONENTS
|
|
||||||
// ========================================================================
|
|
||||||
|
|
||||||
/** Visual representation and physics body */
|
|
||||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Pickup")
|
|
||||||
TObjectPtr<UStaticMeshComponent> MeshComp;
|
|
||||||
|
|
||||||
/** Trigger zone for detection */
|
|
||||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Pickup")
|
|
||||||
TObjectPtr<USphereComponent> TriggerComp;
|
|
||||||
|
|
||||||
// ========================================================================
|
|
||||||
// STATE API
|
|
||||||
// ========================================================================
|
|
||||||
|
|
||||||
/** * Called when character picks up this item.
|
|
||||||
* Disables physics and collision.
|
|
||||||
*/
|
|
||||||
UFUNCTION(BlueprintCallable, Category = "Pickup")
|
|
||||||
virtual void OnPickedUp(USceneComponent* AttachTo, FName SocketName);
|
|
||||||
|
|
||||||
/** * Called when character drops/throws this item.
|
|
||||||
* Re-enables physics and detaches.
|
|
||||||
* @param Impulse - Optional force to apply (for throwing)
|
|
||||||
* @param bVelChange
|
|
||||||
*/
|
|
||||||
UFUNCTION(BlueprintCallable, Category = "Pickup")
|
|
||||||
virtual void OnDropped(FVector Impulse = FVector::ZeroVector, bool bVelChange = false);
|
|
||||||
|
|
||||||
/** Check if currently held by someone */
|
|
||||||
UFUNCTION(BlueprintPure, Category = "Pickup")
|
|
||||||
bool IsHeld() const { return bIsHeld; }
|
|
||||||
|
|
||||||
protected:
|
|
||||||
bool bIsHeld = false;
|
|
||||||
};
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
// Source/TengriPlatformer/World/TengriPickupActor.ts
|
|
||||||
|
|
||||||
import { Actor } from '/Content/UE/Actor';
|
|
||||||
|
|
||||||
export class TengriPickupActor extends Actor {}
|
|
||||||
Loading…
Reference in New Issue