feat(interaction): implement pickup, aim and throw mechanics
- Implement Context-Based Input system: - Added `IMC_ItemHeld` mapping context. - Added logic to switch contexts when picking up/dropping items. - Added `Throw` (LMB/R2) and `Aim` (RMB/L2) input actions. - Refactor Interaction Logic: - `Interact`: Now handles picking up items or gently dropping them if held. - `OnThrowInput`: Implemented physics-based throw towards camera aim point (center of screen raycast). - Added character rotation to face the throw target instantly. - Update Physics & Movement: - Added `bStrafing` mode to `TengriMovementComponent` to allow rotation towards camera view. - Updated `TengriPickupActor` to use `bVelChange` for impulses (ignoring mass for consistent throw arc). - Tuned throw force and added vertical arc bias. - Fixes: - Resolved rotation logic in MovementComponent to support Aiming state.main
parent
c54c9fd6ae
commit
8744cb759b
|
|
@ -0,0 +1,5 @@
|
||||||
|
// Content/BasicShapes/CustomDefaultSkeletalMesh.ts
|
||||||
|
|
||||||
|
import { SkeletalMesh } from '/Content/UE/SkeletalMesh.ts';
|
||||||
|
|
||||||
|
export class CustomDefaultSkeletalMesh extends SkeletalMesh {}
|
||||||
Binary file not shown.
|
|
@ -5,26 +5,34 @@ 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 Pawn {
|
export class BP_MainCharacter extends TengriCharacter {
|
||||||
// ════════════════════════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
// GRAPHS
|
// GRAPHS
|
||||||
// ════════════════════════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
@ -145,6 +153,32 @@ export class BP_MainCharacter extends Pawn {
|
||||||
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)
|
||||||
|
|
@ -170,6 +204,8 @@ export class BP_MainCharacter extends Pawn {
|
||||||
this.InputDeviceComponent,
|
this.InputDeviceComponent,
|
||||||
this.DebugHUDComponent
|
this.DebugHUDComponent
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CreateWidget(WBP_HUD).AddToViewport();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -198,12 +234,32 @@ export class BP_MainCharacter extends Pawn {
|
||||||
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
|
||||||
|
|
@ -222,7 +278,9 @@ export class BP_MainCharacter extends Pawn {
|
||||||
*/
|
*/
|
||||||
ToastSystemComponent = new AC_ToastSystem();
|
ToastSystemComponent = new AC_ToastSystem();
|
||||||
|
|
||||||
TengriMovement = new TengriMovementComponent(DA_TengriMovementConfig);
|
TengriMovement = new TengriMovementComponent({
|
||||||
|
MovementConfig: DA_TengriMovementConfig,
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Debug HUD system - displays movement parameters and performance metrics
|
* Debug HUD system - displays movement parameters and performance metrics
|
||||||
|
|
@ -230,12 +288,6 @@ export class BP_MainCharacter extends Pawn {
|
||||||
*/
|
*/
|
||||||
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
|
||||||
|
|
@ -243,8 +295,43 @@ export class BP_MainCharacter extends Pawn {
|
||||||
*/
|
*/
|
||||||
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.
|
|
@ -0,0 +1,5 @@
|
||||||
|
// Content/Blueprints/BP_Rock.ts
|
||||||
|
|
||||||
|
import { TengriPickupActor } from '/Source/TengriPlatformer/World/TengriPickupActor.ts';
|
||||||
|
|
||||||
|
export class BP_Rock extends TengriPickupActor {}
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,6 @@
|
||||||
|
// 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'));
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,6 @@
|
||||||
|
// 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'));
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,5 @@
|
||||||
|
// 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'));
|
||||||
Binary file not shown.
|
|
@ -11,6 +11,7 @@ import { IA_ToggleVisualDebug } from '/Content/Input/Actions/IA_ToggleVisualDebu
|
||||||
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_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,3 +24,4 @@ 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_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.
|
|
@ -0,0 +1,11 @@
|
||||||
|
// 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'));
|
||||||
Binary file not shown.
|
|
@ -1,5 +1,7 @@
|
||||||
// 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.
|
|
@ -0,0 +1,10 @@
|
||||||
|
// 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,6 +114,50 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,343 @@
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,161 @@
|
||||||
|
// 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;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
@ -48,8 +48,7 @@ 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;
|
||||||
|
|
@ -60,7 +59,7 @@ FTengriSweepResult UTengriCollisionResolver::PerformSweep(
|
||||||
return Result;
|
return Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
UWorld* World = WorldContext->GetWorld();
|
const UWorld* World = WorldContext->GetWorld();
|
||||||
if (!World)
|
if (!World)
|
||||||
{
|
{
|
||||||
return Result;
|
return Result;
|
||||||
|
|
@ -118,7 +117,7 @@ bool UTengriCollisionResolver::StepUp(
|
||||||
const FVector& DesiredDelta,
|
const FVector& DesiredDelta,
|
||||||
const FHitResult& ImpactHit,
|
const FHitResult& ImpactHit,
|
||||||
const UCapsuleComponent* Capsule,
|
const UCapsuleComponent* Capsule,
|
||||||
float MaxStepHeight,
|
const float MaxStepHeight,
|
||||||
FVector& OutLocation)
|
FVector& OutLocation)
|
||||||
{
|
{
|
||||||
// Reject sloped surfaces and overhangs
|
// Reject sloped surfaces and overhangs
|
||||||
|
|
@ -136,7 +135,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, false);
|
const FTengriSweepResult UpSweep = PerformSweep(WorldContext, StartLocation, StepUpEnd, Capsule);
|
||||||
|
|
||||||
// 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))
|
||||||
|
|
@ -157,7 +156,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, false);
|
const FTengriSweepResult ForwardSweep = PerformSweep(WorldContext, UpSweep.Location, ForwardEnd, Capsule);
|
||||||
|
|
||||||
// Reject if obstacle continues upward (wall, next stair step)
|
// Reject if obstacle continues upward (wall, next stair step)
|
||||||
if (ForwardSweep.bBlocked)
|
if (ForwardSweep.bBlocked)
|
||||||
|
|
@ -168,7 +167,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, false);
|
const FTengriSweepResult DownSweep = PerformSweep(WorldContext, DownStart, DownEnd, Capsule);
|
||||||
|
|
||||||
if (!DownSweep.bBlocked || DownSweep.Hit.ImpactNormal.Z < TengriPhysics::MinGroundNormalZ)
|
if (!DownSweep.bBlocked || DownSweep.Hit.ImpactNormal.Z < TengriPhysics::MinGroundNormalZ)
|
||||||
{
|
{
|
||||||
|
|
@ -227,7 +226,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, false); Sweep.bBlocked && Thresholds.IsWalkable(Sweep.Hit.ImpactNormal.Z))
|
if (const FTengriSweepResult Sweep = PerformSweep(WorldContext, Start, End, Capsule); Sweep.bBlocked && Thresholds.IsWalkable(Sweep.Hit.ImpactNormal.Z))
|
||||||
{
|
{
|
||||||
OutLocation = Sweep.Location;
|
OutLocation = Sweep.Location;
|
||||||
OutHit = Sweep.Hit;
|
OutHit = Sweep.Hit;
|
||||||
|
|
@ -278,9 +277,7 @@ 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)
|
||||||
{
|
{
|
||||||
FVector HorizontalTangent = FVector::CrossProduct(ImpactNormal, FVector(0.f, 0.f, 1.f)).GetSafeNormal();
|
if (FVector HorizontalTangent = FVector::CrossProduct(ImpactNormal, FVector(0.f, 0.f, 1.f)).GetSafeNormal(); !HorizontalTangent.IsNearlyZero())
|
||||||
|
|
||||||
if (!HorizontalTangent.IsNearlyZero())
|
|
||||||
{
|
{
|
||||||
if (FVector::DotProduct(HorizontalTangent, RemainingDelta) < 0.f)
|
if (FVector::DotProduct(HorizontalTangent, RemainingDelta) < 0.f)
|
||||||
{
|
{
|
||||||
|
|
@ -323,8 +320,7 @@ 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;
|
||||||
|
|
@ -336,7 +332,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, bShowDebug);
|
const FTengriSweepResult Sweep = PerformSweep(WorldContext, CurrentLocation, Target, Capsule);
|
||||||
|
|
||||||
FinalResult.CollisionCount++;
|
FinalResult.CollisionCount++;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,6 @@ 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(
|
||||||
|
|
@ -45,8 +44,7 @@ public:
|
||||||
const UCapsuleComponent* Capsule,
|
const UCapsuleComponent* Capsule,
|
||||||
const FSurfaceThresholds& Thresholds,
|
const FSurfaceThresholds& Thresholds,
|
||||||
float MaxStepHeight,
|
float MaxStepHeight,
|
||||||
int32 MaxIterations,
|
int32 MaxIterations
|
||||||
bool bShowDebug = false
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
|
@ -58,8 +56,7 @@ 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 */
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,6 @@ void UTengriMovementComponent::SetJumpInput(const bool bPressed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обновляем состояние удержания
|
|
||||||
bIsJumpHeld = bPressed;
|
bIsJumpHeld = bPressed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -138,8 +137,8 @@ void UTengriMovementComponent::SetJumpInput(const bool bPressed)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
void UTengriMovementComponent::TickComponent(
|
void UTengriMovementComponent::TickComponent(
|
||||||
float DeltaTime,
|
const float DeltaTime,
|
||||||
ELevelTick TickType,
|
const ELevelTick TickType,
|
||||||
FActorComponentTickFunction* ThisTickFunction)
|
FActorComponentTickFunction* ThisTickFunction)
|
||||||
{
|
{
|
||||||
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
|
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
|
||||||
|
|
@ -346,27 +345,60 @@ void UTengriMovementComponent::TickPhysics(
|
||||||
PhysicsVelocity = HorizontalVelocity;
|
PhysicsVelocity = HorizontalVelocity;
|
||||||
PhysicsVelocity.Z = CurrentZ;
|
PhysicsVelocity.Z = CurrentZ;
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════
|
||||||
// Phase 4: Rotation
|
// Phase 4: Rotation
|
||||||
// ════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
if (const float MinSpeedSq = FMath::Square(MovementConfig->MinSpeedForRotation);
|
FRotator TargetRot = PhysicsRotation; // Default: maintain current rotation
|
||||||
PhysicsVelocity.SizeSquared2D() > MinSpeedSq)
|
bool bShouldUpdateRotation = false;
|
||||||
{
|
float CurrentRotSpeed = MovementConfig->RotationSpeed;
|
||||||
FRotator TargetRot = PhysicsVelocity.ToOrientationRotator();
|
|
||||||
TargetRot.Pitch = 0.0f;
|
|
||||||
TargetRot.Roll = 0.0f;
|
|
||||||
|
|
||||||
PhysicsRotation = FMath::RInterpConstantTo(
|
// SCENARIO A: STRAFE MODE (Aiming)
|
||||||
PhysicsRotation,
|
// Character rotates to match camera even when stationary.
|
||||||
TargetRot,
|
// Used for combat/precise aiming scenarios.
|
||||||
FixedDeltaTime,
|
if (bStrafing)
|
||||||
MovementConfig->RotationSpeed
|
{
|
||||||
);
|
if (const APawn* PawnOwner = Cast<APawn>(GetOwner()))
|
||||||
}
|
{
|
||||||
|
if (const AController* C = PawnOwner->GetController())
|
||||||
|
{
|
||||||
|
TargetRot = C->GetControlRotation();
|
||||||
|
bShouldUpdateRotation = true;
|
||||||
|
|
||||||
|
// Optional: Faster rotation in combat for responsive feel
|
||||||
|
CurrentRotSpeed *= 2.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// SCENARIO B: STANDARD MOVEMENT (Classic platformer)
|
||||||
|
// 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)
|
||||||
|
{
|
||||||
|
TargetRot = PhysicsVelocity.ToOrientationRotator();
|
||||||
|
bShouldUpdateRotation = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// APPLY ROTATION
|
||||||
|
if (bShouldUpdateRotation)
|
||||||
|
{
|
||||||
|
// Always prevent pitch/roll to keep character upright
|
||||||
|
TargetRot.Pitch = 0.0f;
|
||||||
|
TargetRot.Roll = 0.0f;
|
||||||
|
|
||||||
|
PhysicsRotation = FMath::RInterpConstantTo(
|
||||||
|
PhysicsRotation,
|
||||||
|
TargetRot,
|
||||||
|
FixedDeltaTime,
|
||||||
|
CurrentRotSpeed
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════
|
||||||
// Phase 5: Gravity (FIXED - Apply before collision)
|
// Phase 5: Gravity
|
||||||
// ════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
if (!bIsGrounded)
|
if (!bIsGrounded)
|
||||||
|
|
@ -401,8 +433,7 @@ void UTengriMovementComponent::TickPhysics(
|
||||||
OwnerCapsule,
|
OwnerCapsule,
|
||||||
CachedThresholds,
|
CachedThresholds,
|
||||||
MovementConfig->MaxStepHeight,
|
MovementConfig->MaxStepHeight,
|
||||||
MovementConfig->MaxSlideIterations,
|
MovementConfig->MaxSlideIterations
|
||||||
false
|
|
||||||
);
|
);
|
||||||
|
|
||||||
PhysicsLocation = MoveResult.Location;
|
PhysicsLocation = MoveResult.Location;
|
||||||
|
|
@ -430,7 +461,7 @@ void UTengriMovementComponent::TickPhysics(
|
||||||
}
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════
|
||||||
// Phase 8: State Update (IMPROVED)
|
// Phase 8: State Update
|
||||||
// ════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
const bool bWasGrounded = bIsGrounded;
|
const bool bWasGrounded = bIsGrounded;
|
||||||
|
|
@ -444,7 +475,7 @@ void UTengriMovementComponent::TickPhysics(
|
||||||
const bool bNowGrounded = bJustSnapped || bHitWalkable;
|
const bool bNowGrounded = bJustSnapped || bHitWalkable;
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════
|
||||||
// Phase 9: Landing Detection (ACCUMULATED)
|
// Phase 9: Landing Detection
|
||||||
// ════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
if (!bWasGrounded && bNowGrounded)
|
if (!bWasGrounded && bNowGrounded)
|
||||||
|
|
@ -524,8 +555,7 @@ 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))
|
||||||
|
|
@ -538,3 +568,11 @@ bool UTengriMovementComponent::PerformGroundSnapping(
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void UTengriMovementComponent::ForceRotation(const FRotator& NewRotation)
|
||||||
|
{
|
||||||
|
// Обновляем все внутренние состояния, чтобы физика "подхватила" новый поворот
|
||||||
|
PhysicsRotation = NewRotation;
|
||||||
|
RenderRotation = NewRotation;
|
||||||
|
PreviousPhysicsRotation = NewRotation; // Сбрасываем интерполяцию
|
||||||
|
}
|
||||||
|
|
@ -36,9 +36,18 @@ public:
|
||||||
UPROPERTY(BlueprintAssignable, Category = "Tengri Movement|Events")
|
UPROPERTY(BlueprintAssignable, Category = "Tengri Movement|Events")
|
||||||
FOnTengriLandingSignature OnLanded;
|
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;
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,25 @@
|
||||||
// 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 type { Vector } from '/Content/UE/Vector.ts';
|
import { 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(MovementConfig: typeof DA_TengriMovementConfig) {
|
constructor(data?: ConstructorParams) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
console.log(MovementConfig);
|
console.log(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
public SetInputVector(NewInput: Vector): void {
|
public SetInputVector(NewInput: Vector = new Vector()): void {
|
||||||
console.log(NewInput);
|
console.log(NewInput);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public SetJumpInput(bPressed: boolean = false): void {
|
||||||
|
console.log(bPressed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,13 @@ 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(new string[] { "Core", "CoreUObject", "Engine", "InputCore" });
|
PublicDependencyModuleNames.AddRange(["Core", "CoreUObject", "Engine", "InputCore"]);
|
||||||
|
|
||||||
PrivateDependencyModuleNames.AddRange(new string[] { });
|
PrivateDependencyModuleNames.AddRange(["EnhancedInput"]);
|
||||||
|
|
||||||
// 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" });
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
// 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;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
// Source/TengriPlatformer/World/TengriPickupActor.ts
|
||||||
|
|
||||||
|
import { Actor } from '/Content/UE/Actor';
|
||||||
|
|
||||||
|
export class TengriPickupActor extends Actor {}
|
||||||
Loading…
Reference in New Issue