Ghost Shot

3D Platformer

Video created and voiced by me.

A cozy 3D platformer set in a sandbox of supernatural abilities. Hop across the land on a journey to photograph ghosts and set them free.

Halloween 2024

CONTENTS

Responsive 3D Movement

Problem Statement

Fluid character movement is the cornerstone of effective platformer design.

Player Experience Goals

  • Predict the Player

    Buffer inputs and logically blend between actions to make movement feel effortless.

  • Provide Clear Feedback

    Movement should provide visual and audio cues to improve the player's sense of control.

  • Achieve Low Input Latency

    Translate player input immediately into visible action, decreasing the friction between themselves and the game experience.

I designed a number of responsive systems that increase the level of control the player has over the character.

A key challenge for me while designing movement — our character is 2D!

How can we effectively convey the direction the player is running in, the speed of the character, and more in 3D space with only a 2D sprite?

I used a variety of visual cues paired with audio to convey the character’s movement state to the player, allowing for strong input feedback despite using a flat sprite.

This contributed greatly to a sense of decreased input latency and increased control over the character.

Designing a Good Jump

Level design plays a large role in how a game feels to play. Jumping is the most common action a player will do, and I therefore wanted it to feel as good as possible.

An early playtest finding was that the amount of space available for landing on a platform influences the intensity of the challenge.

This diagram is scaled to 50 Unreal Units Squared per tile, and was used as reference for designing platforming challenges.

The movement system I programmed halves gravity at the peak of a jump, which means that the jump apex is when the player has the most control. From playtest feedback and iteration, I determined that the ideal jump occurs when the player character is at the apex of their jump right before landing on a platform.

All of our jumps follow this principle. We modify the landing sizes to vary intensity and design each jump to coincide with where a player will reach the apex just as they arrive over a platform.

Designing Effortless Controls

Our user experience research revealed that the bottom and left face buttons (A and X), the right trigger, and the left thumbstick are the most easy to reach buttons on a standard controller.

I committed to using those buttons as the primary movement buttons, each with its own role. This ensured general movement was as comfortable as possible on a controller.

From there, I developed an advanced moveset that used combinations of the primary movement buttons. This allowed for more movement possibilities, such as spinning in midair, diving, crawling, and more.

Additionally, some buttons, such as jump, provided more of that input the longer they were held down.

With just the four most reachable controller buttons, I was able to achieve an expressive moveset that combines each move in meaningful ways.

In our playtesting, this drastically increased the ease of control the player had. More importantly, the complex moveset removed friction between movement states (jumping, walking, etc.), allowing players to seamlessly transition into the next move they wanted to execute.

C++ Gameplay Framework

Engineering Approach

My approach to gameplay programming was to build or refactor all core gameplay functionality as native C++ code. This code is designed to be extended in Blueprint to allow for rapid prototyping.

To accomplish this, code needed to be written in a way that made it easy to compose by calling functions exposed to Blueprints.

Engineering Goals

  • Extensibility

    Code built to be extended with new functionality, including new mechanics and prototypes.

  • Performance

    Code that is cache-coherent and uses the minimal amount of clock cycles.

  • Good Documentation

    Good comments ensure other members of the team can work productively with the codebase.

Player Character Class

The player character is made up of a core C++ class (extends APawn), and then composed of multiple actor components that each encapsulate some functionality.

For example, player movement and input handling is done by the PlayerMovementController class. The player’s graphical representation and visual effects is done in PlayerGraphicsController.

This approach allows programmers on the team to reason about encapsulated chunks of functionality, rather than deal with the complexity of the entire character class and everything it does.

Additionally, this also allows for interchangeability among components. Components are not dependent or aware of each other by design, and communicate via event dispatchers in the base character class. This allows each component to be extended, rewritten, or redone to provide new functionality without rewriting other parts of the character class.

  1. // Copyright 2022 Soup Initiative Games
  2.  
  3.  
  4. #include "ProjectSpooky64/Public/Pawns/PlayerMovementController.h"
  5. #include "ProjectSpooky64/Public/Pawns/APlayer_Character.h"
  6. #include "PaperCharacter.h"
  7. #include "Components/CapsuleComponent.h"
  8. #include "GameFramework/Actor.h"
  9. #include "Kismet/KismetMathLibrary.h"
  10. #include "Kismet/GameplayStatics.h"
  11.  
  12.  
  13. // Sets default values for this component's properties
  14. UPlayerMovementController::UPlayerMovementController()
  15. {
  16. // Set this component to be initialized when the game starts, and to be ticked every frame. You can turn these features
  17. // off to improve performance if you don't need them.
  18. PrimaryComponentTick.bCanEverTick = true;
  19.  
  20. // ...
  21. MoveState = EMoveState::Idle;
  22. MoveSet = EMoveSet::Standard;
  23. }
  24.  
  25.  
  26. // Called when the game starts
  27. void UPlayerMovementController::BeginPlay()
  28. {
  29. Super::BeginPlay();
  30.  
  31. //Ref_CharacterMovement = GetOwner()->FindComponentByClass<UCharacterMovementComponent>();
  32. Parent = Cast<AAPlayer_Character>(GetOwner());
  33. Ref_CharacterMovement = Parent->GetCharacterMovement();
  34.  
  35. Parent->JumpMaxHoldTime = JumpBufferMaxTime;
  36.  
  37. Parent->OnMoveStateChanged.AddDynamic(this, &UPlayerMovementController::NotifyStateChanged);
  38. Parent->OnMoveSetChanged.AddDynamic(this, &UPlayerMovementController::NotifySetChanged);
  39. Parent->OnLanded.AddDynamic(this, &UPlayerMovementController::NotifyOnLanded);
  40. Parent->OnJumped.AddDynamic(this, &UPlayerMovementController::NotifyOnJumped);
  41. Parent->OnLaunched.AddDynamic(this, &UPlayerMovementController::NotifyOnLaunched);
  42. Parent->HitMechanic.AddDynamic(this, &UPlayerMovementController::NotifyHitMechanicFired);
  43. Parent->CameraMechanic.AddDynamic(this, &UPlayerMovementController::NotifyCameraMechanicFired);
  44.  
  45. StateFunction = &UPlayerMovementController::State_Idle;
  46. (this->*StateFunction)(EStateInput::INIT);
  47.  
  48.  
  49. }
  50.  
  51.  
  52. // Called every frame
  53. void UPlayerMovementController::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
  54. {
  55. Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
  56. //UE_LOG(LogTemp, Warning, TEXT("Jump buffered: %s"), (JumpInputBuffered) ? TEXT("TRUE") : TEXT("false"));
  57. }
  58.  
  59.  
  60. void UPlayerMovementController::GuardedTick_Implementation(float DeltaTime)
  61. {
  62. _Internal_DeltaTime = DeltaTime;
  63.  
  64. //_SetInputVectorAndRotation(); // todo: might be able to move this to a lower function later
  65.  
  66. _CalculateInputRotationFromX();
  67.  
  68. (this->*StateFunction)(EStateInput::TICK);
  69.  
  70. //UE_LOG(LogTemp, Warning, TEXT("Can the parent jump: %s"), (Parent->CanJump() ? TEXT("true") : TEXT("false")));
  71.  
  72. //_Debug_PrintState();
  73. }
  74.  
  75.  
  76. float UPlayerMovementController::GetInternalDeltaTime()
  77. {
  78. return _Internal_DeltaTime;
  79. }
  80.  
  81.  
  82. void UPlayerMovementController::SetMoveState(EMoveState NewState)
  83. {
  84. MoveState = NewState;
  85. }
  86.  
  87.  
  88. void UPlayerMovementController::SetMoveSet(EMoveSet NewSet)
  89. {
  90. MoveSet = NewSet;
  91. }
  92.  
  93.  
  94. EMoveState UPlayerMovementController::GetMoveState()
  95. {
  96. return MoveState.GetValue();
  97. }
  98.  
  99.  
  100. EMoveSet UPlayerMovementController::GetMoveSet()
  101. {
  102. return MoveSet.GetValue();
  103. }
  104.  
  105.  
  106. void UPlayerMovementController::InitializeInputs(UInputComponent* PlayerInputComponent)
  107. {
  108. UE_LOG(LogTemp, Warning, TEXT("PlayerMovementController initialized."));
  109. PlayerInputComponent->BindAction("Jump", EInputEvent::IE_Pressed, this, &UPlayerMovementController::JumpActionPressed);
  110. PlayerInputComponent->BindAction("Jump", EInputEvent::IE_Released, this, &UPlayerMovementController::JumpActionReleased);
  111. PlayerInputComponent->BindAction("Crouch", EInputEvent::IE_Pressed, this, &UPlayerMovementController::CrouchActionPressed);
  112. PlayerInputComponent->BindAction("Crouch", EInputEvent::IE_Released, this, &UPlayerMovementController::CrouchActionReleased);
  113. PlayerInputComponent->BindAction("Flash", EInputEvent::IE_Pressed, this, &UPlayerMovementController::FlashActionPressed);
  114. PlayerInputComponent->BindAction("Flash", EInputEvent::IE_Released, this, &UPlayerMovementController::FlashActionReleased);
  115. PlayerInputComponent->BindAction("Grab", EInputEvent::IE_Pressed, this, &UPlayerMovementController::GrabActionPressed);
  116. PlayerInputComponent->BindAction("Grab", EInputEvent::IE_Released, this, &UPlayerMovementController::GrabActionReleased);
  117. PlayerInputComponent->BindAction("Hit", EInputEvent::IE_Pressed, this, &UPlayerMovementController::HitActionPressed);
  118. PlayerInputComponent->BindAction("Hit", EInputEvent::IE_Released, this, &UPlayerMovementController::HitActionReleased);
  119. PlayerInputComponent->BindAxis("HorizontalMovement", this, &UPlayerMovementController::HorizontalAxisInput);
  120. PlayerInputComponent->BindAxis("VerticalMovement", this, &UPlayerMovementController::VerticalAxisInput);
  121. }
  122.  
  123.  
  124. void UPlayerMovementController::JumpActionPressed()
  125. {
  126. if (!InputEnabled) { return; }
  127. (this->*StateFunction)(EStateInput::JUMP_Pressed);
  128. }
  129.  
  130.  
  131. void UPlayerMovementController::JumpActionReleased()
  132. {
  133. if (!InputEnabled) { return; }
  134. (this->*StateFunction)(EStateInput::JUMP_Released);
  135. }
  136.  
  137.  
  138. void UPlayerMovementController::CrouchActionPressed()
  139. {
  140. if (!InputEnabled) { return; }
  141. (this->*StateFunction)(EStateInput::CROUCH_Pressed);
  142. //UE_LOG(LogTemp, Warning, TEXT("Crouch action pressed"));
  143. }
  144.  
  145.  
  146. void UPlayerMovementController::CrouchActionReleased()
  147. {
  148. if (!InputEnabled) { return; }
  149. (this->*StateFunction)(EStateInput::CROUCH_Released);
  150. }
  151.  
  152.  
  153. void UPlayerMovementController::FlashActionPressed()
  154. {
  155. if (!InputEnabled) { return; }
  156. (this->*StateFunction)(EStateInput::FLASH_Pressed);
  157. }
  158.  
  159.  
  160. void UPlayerMovementController::FlashActionReleased()
  161. {
  162. if (!InputEnabled) { return; }
  163. (this->*StateFunction)(EStateInput::FLASH_Released);
  164. Parent->ReleaseCameraMechanic();
  165. }
  166.  
  167.  
  168. void UPlayerMovementController::GrabActionPressed()
  169. {
  170. if (!InputEnabled) { return; }
  171. (this->*StateFunction)(EStateInput::GRAB_Pressed);
  172. Parent->FireAimMechanic(); // Might want to refactor into a state helper function.
  173. UE_LOG(LogTemp, Warning, TEXT("Grab action pressed"));
  174. }
  175.  
  176.  
  177. void UPlayerMovementController::GrabActionReleased()
  178. {
  179. if (!InputEnabled) { return; }
  180. (this->*StateFunction)(EStateInput::GRAB_Released);
  181. Parent->ReleaseAimMechanic(); // Might want to refactor into a state helper function.
  182. UE_LOG(LogTemp, Warning, TEXT("Grab action released"));
  183. }
  184.  
  185.  
  186. void UPlayerMovementController::HitActionPressed()
  187. {
  188. if (!InputEnabled) { return; }
  189. (this->*StateFunction)(EStateInput::HIT_Pressed);
  190. }
  191.  
  192.  
  193. void UPlayerMovementController::HitActionReleased()
  194. {
  195. if (!InputEnabled) { return; }
  196. (this->*StateFunction)(EStateInput::HIT_Released);
  197. Parent->ReleaseHitMechanic();
  198. }
  199.  
  200.  
  201. void UPlayerMovementController::HorizontalAxisInput(float AxisValue)
  202. {
  203.  
  204. }
  205.  
  206.  
  207. void UPlayerMovementController::VerticalAxisInput(float AxisValue)
  208. {
  209.  
  210. }
  211.  
  212.  
  213. void UPlayerMovementController::SetMoveEnabled(bool Enabled)
  214. {
  215. if (Enabled)
  216. {
  217. this->InputEnabled = true;
  218. Parent->_SetMovementInputEnabled(true);
  219. }
  220. else
  221. {
  222. this->InputEnabled = false;
  223. Parent->_SetMovementInputEnabled(false);
  224. ResetMovement();
  225. }
  226. }
  227.  
  228.  
  229. float UPlayerMovementController::GetHorizontalAxisValue()
  230. {
  231. if (!InputEnabled) { return 0.f; }
  232. return Parent->GetInputAxisValue("HorizontalMovement");
  233. }
  234.  
  235. float UPlayerMovementController::GetVerticalAxisValue()
  236. {
  237. if (!InputEnabled) { return 0.f; }
  238. return Parent->GetInputAxisValue("VerticalMovement");
  239. }
  240.  
  241.  
  242. void UPlayerMovementController::ResetMovement()
  243. {
  244. (this->*StateFunction)(EStateInput::RESET);
  245. NewState(EMoveState::Idle);
  246. Ref_CharacterMovement->Velocity = FVector{ 0.f, 0.f, 0.f };
  247. }
  248.  
  249.  
  250. void UPlayerMovementController::State_Idle_Implementation(EStateInput input)
  251. {
  252. switch(MoveSet)
  253. {
  254. case Limited:
  255. Limited_Idle(input);
  256. break;
  257.  
  258. default:
  259. Standard_Idle(input);
  260. break;
  261. }
  262. }
  263.  
  264.  
  265. void UPlayerMovementController::State_Run_Implementation(EStateInput input)
  266. {
  267. switch (MoveSet)
  268. {
  269. case Limited:
  270. Limited_Run(input);
  271. break;
  272.  
  273. default:
  274. Standard_Run(input);
  275. break;
  276. }
  277. }
  278.  
  279.  
  280. void UPlayerMovementController::State_Jump_Implementation(EStateInput input)
  281. {
  282. switch (MoveSet)
  283. {
  284. case Limited:
  285. Limited_Jump(input);
  286. break;
  287.  
  288. default:
  289. Standard_Jump(input);
  290. break;
  291. }
  292. }
  293.  
  294.  
  295. void UPlayerMovementController::State_HighJump_Implementation(EStateInput input)
  296. {
  297. switch (MoveSet)
  298. {
  299. case Limited:
  300. break;
  301.  
  302. default:
  303. Standard_HighJump(input);
  304. break;
  305. }
  306. }
  307.  
  308.  
  309. void UPlayerMovementController::State_SpinJump_Implementation(EStateInput input)
  310. {
  311. switch (MoveSet)
  312. {
  313. case Limited:
  314. break;
  315.  
  316. default:
  317. Standard_SpinJump(input);
  318. break;
  319. }
  320. }
  321.  
  322.  
  323. void UPlayerMovementController::State_Fall_Implementation(EStateInput input)
  324. {
  325. switch (MoveSet)
  326. {
  327. case Limited:
  328. Limited_Fall(input);
  329. break;
  330.  
  331. default:
  332. Standard_Fall(input);
  333. break;
  334. }
  335. }
  336.  
  337.  
  338. void UPlayerMovementController::State_Crouch_Implementation(EStateInput input)
  339. {
  340. switch (MoveSet)
  341. {
  342. case Limited:
  343. Limited_Crouch(input);
  344. break;
  345.  
  346. default:
  347. Standard_Crouch(input);
  348. break;
  349. }
  350. }
  351.  
  352.  
  353. void UPlayerMovementController::State_Crawl_Implementation(EStateInput input)
  354. {
  355. switch (MoveSet)
  356. {
  357. case Limited:
  358. Limited_Crawl(input);
  359. break;
  360.  
  361. default:
  362. Standard_Crawl(input);
  363. break;
  364. }
  365. }
  366.  
  367.  
  368. void UPlayerMovementController::State_Pound_Implementation(EStateInput input)
  369. {
  370. switch (MoveSet)
  371. {
  372. case Limited:
  373. break;
  374.  
  375. default:
  376. Standard_Pound(input);
  377. break;
  378. }
  379. }
  380.  
  381.  
  382. void UPlayerMovementController::State_PoundFlip_Implementation(EStateInput input)
  383. {
  384. switch (MoveSet)
  385. {
  386. case Limited:
  387. break;
  388.  
  389. default:
  390. Standard_PoundFlip(input);
  391. break;
  392. }
  393. }
  394.  
  395.  
  396. void UPlayerMovementController::State_Dive_Implementation(EStateInput input)
  397. {
  398. switch (MoveSet)
  399. {
  400. case Limited:
  401. break;
  402.  
  403. default:
  404. Standard_Dive(input);
  405. break;
  406. }
  407. }
  408.  
  409.  
  410. void UPlayerMovementController::State_Dodge_Implementation(EStateInput input)
  411. {
  412. switch (MoveSet)
  413. {
  414. case Limited:
  415. break;
  416.  
  417. default:
  418. Standard_Dodge(input);
  419. break;
  420. }
  421. }
  422.  
  423.  
  424. /*
  425.  * Note:
  426.  * When extending in Blueprint,
  427.  * call Super/parent version for every case that isn't going to have a unique Blueprint implementation.
  428.  */
  429. void UPlayerMovementController::Standard_Idle_Implementation(EStateInput input)
  430. {
  431. if (CommonActions(input)) { return; }
  432.  
  433. switch (input)
  434. {
  435. case INIT:
  436. Parent->_SetMoveState(EMoveState::Idle);
  437. break;
  438.  
  439. case TICK:
  440. if (Ref_CharacterMovement->IsFalling())
  441. {
  442. NewState(EMoveState::Falling);
  443. return;
  444. }
  445. else if (NormalMovement()) // If movement is being applied
  446. {
  447. NewState(EMoveState::Running);
  448. return;
  449. }
  450. break;
  451.  
  452. case JUMP_Pressed:
  453. NewState(EMoveState::Jumping);
  454. return;
  455.  
  456. case CROUCH_Pressed:
  457. NewState(EMoveState::Crouching);
  458. return;
  459.  
  460. case HIT_Pressed:
  461. // fire companion hit (prob in parent APlayer_Character)
  462. break;
  463.  
  464. case END:
  465.  
  466. break;
  467.  
  468. default:
  469. break;
  470. }
  471. }
  472.  
  473.  
  474. void UPlayerMovementController::Standard_Run_Implementation(EStateInput input)
  475. {
  476. if (CommonActions(input)) { return; }
  477.  
  478. switch (input)
  479. {
  480. case INIT:
  481. Parent->_SetMoveState(EMoveState::Running);
  482. break;
  483.  
  484. case TICK:
  485. if (Ref_CharacterMovement->IsFalling())
  486. {
  487. NewState(EMoveState::Falling);
  488. return;
  489. }
  490. else if (!NormalMovement())
  491. {
  492. NewState(EMoveState::Idle);
  493. return;
  494. }
  495. break;
  496.  
  497. case JUMP_Pressed:
  498. NewState(EMoveState::Jumping);
  499. return;
  500.  
  501. case CROUCH_Pressed:
  502. NewState(EMoveState::Crawling);
  503. break;
  504.  
  505. default:
  506. break;
  507. }
  508. }
  509.  
  510.  
  511. void UPlayerMovementController::Standard_Jump_Implementation(EStateInput input)
  512. {
  513. if (CommonOnLandedActions(input)) { return; }
  514.  
  515. /*
  516. * Returns true if NewState is called in CommonJumpAction's scope, therefore preventing the remainder of this function
  517. * from firing after the state has been changed.
  518. */
  519. if (CommonJumpActions(input)) { return; }
  520.  
  521. if (_RegularJump(input)) { return; }
  522. if (_JumpTransitionGuard(input)) { return; }
  523.  
  524. switch (input)
  525. {
  526. case INIT:
  527. Parent->_SetMoveState(EMoveState::Jumping);
  528. //UE_LOG(LogTemp, Warning, TEXT("Jump max count: %i"), Parent->JumpMaxCount);
  529. //Parent->Jump();
  530. //CharMovement_JumpBuffered = true; // Guard for notifying TICK case that there is a jump waiting to be applied next update
  531. if (!Parent->CanJump())
  532. {
  533. NewState(EMoveState::Idle);
  534. return;
  535. }
  536. break;
  537.  
  538. case TICK:
  539. //UE_LOG(LogTemp, Warning, TEXT("Jump buffered: %s"), (JumpInputBuffered) ? TEXT("TRUE") : TEXT("false"));
  540. if (!Ref_CharacterMovement->IsFalling() && (!JumpInputBuffered || !Parent->CanJump())) //todo: can you refactor this to EVENT_Landed?
  541. {
  542. NewState(EMoveState::Idle);
  543. return;
  544. }
  545. /*todo: if jump is called but cannot be processed, call idle*/
  546. //CharMovement_JumpBuffered = false;
  547. break;
  548.  
  549. case JUMP_Released:
  550. Parent->StopJumping();
  551. break;
  552.  
  553. case CROUCH_Released:
  554.  
  555. break;
  556.  
  557. case EVENT_Landed:
  558. Parent->StopJumping();
  559. break;
  560.  
  561. case EVENT_Jumped:
  562. //CharMovement_JumpBuffered = false;
  563. break;
  564.  
  565. case END:
  566. //CharMovement_JumpBuffered = true;
  567. //Parent->JumpMaxCount = 1;
  568. break;
  569.  
  570. default:
  571. break;
  572. }
  573. }
  574.  
  575.  
  576. bool UPlayerMovementController::_RegularJump(EStateInput input)
  577. {
  578. switch (input)
  579. {
  580. case INIT:
  581. Parent->Jump();
  582. break;
  583.  
  584. case END:
  585. Parent->JumpMaxCount = 1;
  586. break;
  587.  
  588. default:
  589. break;
  590. }
  591. return false;
  592. }
  593.  
  594.  
  595. bool UPlayerMovementController::CommonOnLandedActions_Implementation(EStateInput input)
  596. {
  597. switch (input)
  598. {
  599. case RESET:
  600. case EVENT_Landed:
  601. SpinJumpsAttempted = 0;
  602. DivesAttempted = 0;
  603. Parent->StopJumping();
  604. break;
  605.  
  606. default:
  607. break;
  608. }
  609. return false;
  610. }
  611.  
  612.  
  613. void UPlayerMovementController::Standard_HighJump_Implementation(EStateInput input)
  614. {
  615. if (CommonOnLandedActions(input)) { return; }
  616. if (CommonJumpActions(input)) { return; }
  617.  
  618. switch (input)
  619. {
  620. case INIT:
  621. Parent->_SetMoveState(EMoveState::HighJumping);
  622. Parent->LaunchCharacter(FVector(0.f, 0.f, Ref_CharacterMovement->JumpZVelocity * 1.75), true, true);
  623. break;
  624.  
  625. case TICK:
  626. break;
  627.  
  628. case EVENT_Landed: //todo: is this going to cause errors? revisit if errors occur around highjumping
  629. NewState(EMoveState::Idle);
  630. return;
  631.  
  632. default:
  633. break;
  634. }
  635. }
  636.  
  637.  
  638. void UPlayerMovementController::Standard_SpinJump_Implementation(EStateInput input)
  639. {
  640. if (CommonOnLandedActions(input)) { return; }
  641.  
  642. switch (input)
  643. {
  644. case INIT:
  645. {
  646. SpinJumpsAttempted++;
  647. Parent->_SetMoveState(EMoveState::SpinJumping);
  648. __DefaultGravityScalePeak = GravityScalePeak; // Note: Alters state variables here, which are reset once this state ends.
  649. GravityScalePeak = GravityScalePeak / 2.f;
  650. FVector _Velocity = Ref_CharacterMovement->Velocity;
  651. FVector2D F2D_Velocity{ _Velocity.X, _Velocity.Y };
  652. /*
  653. FVector InputVector = InputRotationFromX.Vector(); // get input from player forward vector
  654. FVector2D F2D_InputVector{ InputVector.X, InputVector.Y }; // make 2D
  655. F2D_Velocity = F2D_Velocity * F2D_InputVector; // @todo: this should redirect velocity in direction of movement
  656. */
  657. FVector2D F2D_VelocityDirection{ 0.f };
  658. float F2D_VelocityMagnitude = 0.f; // we want the magnitude
  659. F2D_Velocity.ToDirectionAndLength(F2D_VelocityDirection, F2D_VelocityMagnitude);
  660. FVector2D F2D_InputVector{ RawInputVector.X, RawInputVector.Y }; // get raw input vector 2D
  661.  
  662. FVector InputVect = InputRotationFromX.Vector();
  663. F2D_Velocity.X = InputVect.X;
  664. F2D_Velocity.Y = InputVect.Y;
  665.  
  666. if (Parent->IsApplyingInput()) // Invalidate X and Y velocity if applying input. Used to make maneuvering easier.
  667. {
  668. /*
  669. F2D_Velocity.X = 0.f;
  670. F2D_Velocity.Y = 0.f;
  671. */
  672. //F2D_Velocity = F2D_InputVector * F2D_VelocityMagnitude; // set velocity in the direction of input
  673. F2D_Velocity = F2D_Velocity * SpinJumpHorizontalVelocity;
  674. }
  675. //F2D_Velocity = F2D_Velocity * 0.65; // dampen velocity
  676. Parent->LaunchCharacter({ F2D_Velocity.X, F2D_Velocity.Y, SpinJumpVerticalVelocity }, true, true);
  677. break;
  678. }
  679.  
  680. case CROUCH_Pressed:
  681. NewState(EMoveState::PoundFlipping);
  682. return;
  683.  
  684. default:
  685. break;
  686. }
  687.  
  688. if (CommonJumpActions(input)) { return; }
  689. // if (_SpinJumpGravityScaling(input)) { return; }
  690.  
  691. switch (input)
  692. {
  693. case EVENT_Landed:
  694. NewState(EMoveState::Idle);
  695. break;
  696.  
  697. case END:
  698. GravityScalePeak = __DefaultGravityScalePeak;
  699. break;
  700.  
  701. default:
  702. break;
  703. }
  704. }
  705.  
  706.  
  707. bool UPlayerMovementController::_SpinJumpGravityScaling(EStateInput input)
  708. {
  709. switch (input)
  710. {
  711. case TICK:
  712. {
  713. FVector CapsuleVelocity = Parent->GetCapsuleComponent()->GetComponentVelocity();
  714. if ((CapsuleVelocity.Z < JumpVelocityUpThreshold) && (CapsuleVelocity.Z > JumpVelocityDownTreshold))
  715. {
  716. Ref_CharacterMovement->GravityScale = GravityScalePeak / 2.f;
  717. }
  718. break;
  719. }
  720.  
  721. default:
  722. break;
  723. }
  724. return false;
  725. }
  726.  
  727.  
  728. void UPlayerMovementController::Standard_Fall_Implementation(EStateInput input)
  729. {
  730. if (CommonOnLandedActions(input)) { return; }
  731. if (CommonJumpActions(input)) { return; }
  732. if (_CoyoteTime(input)) { return; }
  733.  
  734. switch (input)
  735. {
  736. case INIT:
  737. Parent->_SetMoveState(EMoveState::Falling);
  738.  
  739. break;
  740.  
  741. case TICK:
  742. if (!Ref_CharacterMovement->IsFalling())
  743. {
  744. NewState(EMoveState::Idle);
  745. return;
  746. }
  747. break;
  748.  
  749. case END:
  750.  
  751. break;
  752.  
  753. default:
  754. break;
  755. }
  756. }
  757.  
  758.  
  759. bool UPlayerMovementController::_CoyoteTime(EStateInput input)
  760. {
  761. switch (input)
  762. {
  763. case TICK:
  764. _CoyoteTimeAccumulation += _Internal_DeltaTime;
  765. break;
  766.  
  767. case JUMP_Pressed:
  768. if (_CoyoteTimeAccumulation < CoyoteTimeDuration)
  769. {
  770. Parent->JumpMaxCount = 2;
  771. NewState(EMoveState::Jumping);
  772. return true;
  773. }
  774. break;
  775.  
  776. case END:
  777. _CoyoteTimeAccumulation = 0.f;
  778. break;
  779.  
  780. default:
  781. break;
  782. }
  783. return false;
  784. }
  785.  
  786.  
  787. void UPlayerMovementController::Standard_Crouch_Implementation(EStateInput input)
  788. {
  789. if (CommonActions(input)) { return; }
  790.  
  791. switch (input)
  792. {
  793. case INIT:
  794. Parent->_SetMoveState(EMoveState::Crouching);
  795. Parent->Crouch();
  796. break;
  797.  
  798. case TICK:
  799. if (NormalMovement())
  800. {
  801. NewState(EMoveState::Crawling);
  802. return;
  803. }
  804. break;
  805.  
  806. case CROUCH_Released:
  807. Parent->UnCrouch();
  808. NewState(EMoveState::Idle);
  809. return;
  810.  
  811. case JUMP_Pressed:
  812. Parent->UnCrouch();
  813. NewState(EMoveState::HighJumping);
  814. return;
  815.  
  816. case END:
  817. //Parent->UnCrouch(); //todo: what if transitioning to crawl? does uncrouch, then crouch work?
  818. break;
  819.  
  820. case RESET:
  821. Parent->UnCrouch();
  822. break;
  823.  
  824. default:
  825. break;
  826. }
  827. }
  828.  
  829.  
  830. void UPlayerMovementController::Standard_Crawl_Implementation(EStateInput input)
  831. {
  832. if (CommonActions(input)) { return; }
  833.  
  834. switch (input)
  835. {
  836. case INIT:
  837. Parent->_SetMoveState(EMoveState::Crawling);
  838. Parent->Crouch();
  839. break;
  840.  
  841. case TICK:
  842. if (!NormalMovement())
  843. {
  844. NewState(EMoveState::Crouching);
  845. return;
  846. }
  847. break;
  848.  
  849. case CROUCH_Released:
  850. Parent->UnCrouch();
  851. NewState(EMoveState::Running);
  852. break;
  853.  
  854. case JUMP_Pressed:
  855. Parent->UnCrouch();
  856. NewState(EMoveState::Dodging);
  857. return;
  858.  
  859. case END:
  860. //Parent->UnCrouch();
  861. break;
  862.  
  863. case RESET:
  864. Parent->UnCrouch();
  865. break;
  866.  
  867. default:
  868. break;
  869. }
  870. }
  871.  
  872.  
  873. void UPlayerMovementController::Standard_Pound_Implementation(EStateInput input)
  874. {
  875. if (CommonOnLandedActions(input)) { return; }
  876.  
  877. switch (input)
  878. {
  879. case INIT:
  880. Parent->_SetMoveState(EMoveState::Pounding);
  881. Parent->LaunchCharacter({ 0.f, 0.f, ((DownwardLaunchVelocity <= 0.f) ? DownwardLaunchVelocity : (DownwardLaunchVelocity * -1.f)) }, false, true);
  882. break;
  883.  
  884. case EVENT_Landed:
  885. NewState(EMoveState::Idle);
  886. break;
  887.  
  888. default:
  889. break;
  890. }
  891. }
  892.  
  893.  
  894. void UPlayerMovementController::Standard_PoundFlip_Implementation(EStateInput input)
  895. {
  896. switch (input)
  897. {
  898. case INIT:
  899. Parent->_SetMoveState(EMoveState::PoundFlipping);
  900. Ref_CharacterMovement->Velocity = { 0.f, 0.f, 0.f };
  901. Ref_CharacterMovement->GravityScale = 0.f;
  902. Parent->GetWorldTimerManager().SetTimer(PoundFlipTimer, this, &UPlayerMovementController::PoundFlipTimerHandler, PoundFlipDuration, false);
  903. break;
  904.  
  905. case JUMP_Pressed:
  906. if (DivesAttempted < MaxDives) {
  907. NewState(EMoveState::Diving);
  908. }
  909. break;
  910.  
  911. case TIMER_PoundFlip:
  912. NewState(EMoveState::Pounding);
  913. break;
  914.  
  915. case END:
  916. Parent->GetWorldTimerManager().ClearTimer(PoundFlipTimer);
  917. Ref_CharacterMovement->GravityScale = 1.f;
  918. break;
  919.  
  920. default:
  921. break;
  922. }
  923. }
  924.  
  925.  
  926. void UPlayerMovementController::PoundFlipTimerHandler()
  927. {
  928. (this->*StateFunction)(EStateInput::TIMER_PoundFlip);
  929. }
  930.  
  931.  
  932. void UPlayerMovementController::Standard_Dive_Implementation(EStateInput input)
  933. {
  934. if (CommonOnLandedActions(input)) { return; }
  935. if (__JumpBuffer(input)) { return; }
  936. if (__JumpGravityScaling(input)) { return; }
  937. if (__StickyLanding(input)) { return; }
  938.  
  939. switch (input)
  940. {
  941. case INIT:
  942. Parent->_SetMoveState(EMoveState::Diving);
  943. DivesAttempted++;
  944.  
  945. if (Parent->IsApplyingInput())
  946. {
  947. // If applying input, dive in input direction
  948. FRotator ControlRotation = Parent->GetControlRotation();
  949. FRotator NormalizedControlRotation{ 0.f, ControlRotation.Yaw, 0.f };
  950. FVector ControlRightVector = UKismetMathLibrary::GetRightVector(NormalizedControlRotation);
  951. FVector ControlForwardVector = UKismetMathLibrary::GetForwardVector(NormalizedControlRotation);
  952. float HorizontalAxisInput = GetHorizontalAxisValue();
  953. float VerticalAxisInput = GetVerticalAxisValue();
  954. FVector InputVector = ((ControlRightVector * HorizontalAxisInput) + (ControlForwardVector * VerticalAxisInput)); // Calc input vector
  955. FVector2D Normalized2DInputVector{ InputVector.X, InputVector.Y }; // We want to normalize the 2D components of the input vector
  956. UKismetMathLibrary::Normalize2D(Normalized2DInputVector);
  957. FVector DiveVector{ (Normalized2DInputVector.X * DiveForwardVelocity), (Normalized2DInputVector.Y * DiveForwardVelocity), DiveZVelocity };
  958. Parent->LaunchCharacter(DiveVector, true, true);
  959. }
  960. else
  961. {
  962. // Otherwise, dive in the direction currently facing
  963. FVector CapsuleForwardVector = Parent->GetCapsuleComponent()->GetForwardVector();
  964. FVector DiveVector = CapsuleForwardVector * DiveForwardVelocity;
  965. DiveVector.Z = DiveZVelocity;
  966. Parent->LaunchCharacter(DiveVector, true, true);
  967. }
  968.  
  969. break;
  970.  
  971. case CROUCH_Pressed:
  972. NewState(EMoveState::PoundFlipping);
  973. return;
  974.  
  975. case HIT_Pressed:
  976. if (SpinJumpsAttempted < MaxSpinJumps)
  977. {
  978. Parent->FireHitMechanic();
  979. }
  980. break;
  981.  
  982. case EVENT_HitMechanic:
  983. NewState(EMoveState::SpinJumping);
  984. return;
  985.  
  986. case EVENT_Landed:
  987. NewState(EMoveState::Idle);
  988. return;
  989.  
  990. default:
  991. break;
  992. }
  993. }
  994.  
  995.  
  996. void UPlayerMovementController::Standard_Dodge_Implementation(EStateInput input)
  997. {
  998. if (CommonOnLandedActions(input)) { return; }
  999. if (_JumpTransitionGuard(input)) { return; }
  1000. if (__JumpBuffer(input)) { return; }
  1001. switch (input)
  1002. {
  1003. case INIT:
  1004. {
  1005. Parent->_SetMoveState(EMoveState::Dodging);
  1006. UCapsuleComponent* _CapsuleComponent = Parent->GetCapsuleComponent();
  1007. FVector CapsuleForwardVector = _CapsuleComponent->GetForwardVector();
  1008. CapsuleForwardVector = CapsuleForwardVector * DodgeForwardVelocity;
  1009. FVector _LaunchVector{ CapsuleForwardVector.X, CapsuleForwardVector.Y, DodgeUpVelocity };
  1010. Parent->LaunchCharacter(_LaunchVector, true, false);
  1011. break;
  1012. }
  1013.  
  1014. case TICK:
  1015. if (!Ref_CharacterMovement->IsFalling() && !JumpInputBuffered)
  1016. {
  1017. NewState(EMoveState::Idle);
  1018. return;
  1019. }
  1020. break;
  1021.  
  1022. case EVENT_Landed:
  1023. NewState(EMoveState::Idle);
  1024. return;
  1025.  
  1026. default:
  1027. break;
  1028. }
  1029. }
  1030.  
  1031.  
  1032. bool UPlayerMovementController::CommonActions_Implementation(EStateInput input)
  1033. {
  1034. switch(input)
  1035. {
  1036. case INIT:
  1037.  
  1038. break;
  1039.  
  1040. case HIT_Pressed:
  1041. Parent->FireHitMechanic();
  1042. break;
  1043.  
  1044. case FLASH_Pressed:
  1045. Parent->FireCameraMechanic();
  1046. break;
  1047.  
  1048. default:
  1049. break;
  1050. }
  1051. return false;
  1052. }
  1053.  
  1054.  
  1055. bool UPlayerMovementController::CommonJumpActions_Implementation(EStateInput input)
  1056. {
  1057. if (__JumpBuffer(input)) { return true; }
  1058. if (__JumpGravityScaling(input)) { return true; }
  1059. if (__StickyLanding(input)) { return true; }
  1060.  
  1061. switch (input)
  1062. {
  1063. case TICK:
  1064. AirMovement();
  1065. break;
  1066.  
  1067. case END:
  1068. //Parent->JumpMaxCount = 1;
  1069. //Parent->StopJumping();
  1070. break;
  1071.  
  1072. case HIT_Pressed:
  1073. if (SpinJumpsAttempted < MaxSpinJumps) {
  1074. Parent->FireHitMechanic();
  1075. }
  1076. break;
  1077.  
  1078. case FLASH_Pressed:
  1079. Parent->FireCameraMechanic();
  1080. break;
  1081.  
  1082. case CROUCH_Pressed:
  1083. NewState(EMoveState::PoundFlipping);
  1084. return true;
  1085.  
  1086. case EVENT_HitMechanic:
  1087. NewState(EMoveState::SpinJumping);
  1088. return true;
  1089.  
  1090. default:
  1091. break;
  1092. }
  1093. return false;
  1094. }
  1095.  
  1096.  
  1097. bool UPlayerMovementController::__StickyLanding(EStateInput input)
  1098. {
  1099. switch (input)
  1100. {
  1101. case EVENT_Landed:
  1102. if (!Parent->IsApplyingInput())
  1103. {
  1104. FVector CharacterMovementVelocity = Ref_CharacterMovement->Velocity;
  1105. Ref_CharacterMovement->Velocity = FVector(0.f, 0.f, CharacterMovementVelocity.Z);
  1106. }
  1107. break;
  1108.  
  1109. default:
  1110. break;
  1111. }
  1112. return false;
  1113. }
  1114.  
  1115.  
  1116. bool UPlayerMovementController::__JumpBuffer(EStateInput input)
  1117. {
  1118. switch (input)
  1119. {
  1120. case INIT:
  1121.  
  1122. break;
  1123.  
  1124. case TICK:
  1125. if (_JumpBuffered)
  1126. {
  1127. _JumpBufferTime += _Internal_DeltaTime;
  1128. }
  1129. break;
  1130.  
  1131. case JUMP_Pressed:
  1132. if (_JumpBuffered)
  1133. {
  1134. _JumpBufferTime = 0.f;
  1135. }
  1136. else
  1137. {
  1138. _JumpBuffered = true;
  1139. }
  1140. break;
  1141.  
  1142. case EVENT_Landed:
  1143. if (_JumpBuffered && (_JumpBufferTime < JumpBufferMaxTime))
  1144. {
  1145. NewState(EMoveState::Jumping);
  1146. return true;
  1147. }
  1148. break;
  1149.  
  1150. case END:
  1151. _JumpBuffered = false;
  1152. _JumpBufferTime = 0.f;
  1153. Parent->StopJumping();
  1154. break;
  1155.  
  1156. default:
  1157. break;
  1158. }
  1159. return false;
  1160. }
  1161.  
  1162.  
  1163. bool UPlayerMovementController::__JumpGravityScaling(EStateInput input)
  1164. {
  1165. switch (input)
  1166. {
  1167. case INIT:
  1168.  
  1169. break;
  1170.  
  1171. case TICK:
  1172. {
  1173. FVector CapsuleVelocity = Parent->GetCapsuleComponent()->GetComponentVelocity();
  1174. if (CapsuleVelocity.Z > JumpVelocityUpThreshold)
  1175. {
  1176. Ref_CharacterMovement->GravityScale = GravityScaleUp;
  1177. }
  1178. else if (CapsuleVelocity.Z < JumpVelocityDownTreshold)
  1179. {
  1180. Ref_CharacterMovement->GravityScale = GravityScaleDown;
  1181. }
  1182. else
  1183. {
  1184. Ref_CharacterMovement->GravityScale = GravityScalePeak;
  1185. }
  1186. break;
  1187. }
  1188. case END:
  1189. Ref_CharacterMovement->GravityScale = 1.f;
  1190. break;
  1191.  
  1192. default:
  1193. break;
  1194. }
  1195. return false;
  1196. }
  1197.  
  1198.  
  1199. bool UPlayerMovementController::_JumpTransitionGuard(EStateInput input)
  1200. {
  1201. switch(input)
  1202. {
  1203. case INIT:
  1204. JumpInputBuffered = true;
  1205. break;
  1206.  
  1207. case EVENT_Jumped:
  1208. //JumpInputBuffered = false;
  1209. break;
  1210.  
  1211. case EVENT_Launched:
  1212. JumpInputBuffered = false;
  1213. break;
  1214.  
  1215. case EVENT_Landed:
  1216. JumpInputBuffered = false;
  1217. break;
  1218.  
  1219. case END:
  1220. JumpInputBuffered = false;
  1221. break;
  1222.  
  1223. default:
  1224. break;
  1225. }
  1226. return false;
  1227. }
  1228.  
  1229.  
  1230. bool UPlayerMovementController::NormalMovement()
  1231. {
  1232. if (Parent->IsApplyingInput()) // todo: does this applying input check have to happen?
  1233. {
  1234. FRotator PlayerForwardRotation = UKismetMathLibrary::MakeRotFromX(Parent->GetCapsuleComponent()->GetForwardVector());
  1235.  
  1236. /* Rotation between input vector and character forward vector. */
  1237. float AbsRotation = _GetAbsoluteRotationBetweenYaws(PlayerForwardRotation, InputRotationFromX);
  1238.  
  1239. if (Parent->GetVelocityVectorLength() < 300.f) // if moving slowly
  1240. {
  1241. _RotateInstantToInputDirection(); // just rotate to the input direction
  1242. _AddForwardMovement();
  1243. }
  1244. else if (AbsRotation > 170.f) // otherwise, if applying input directly opposite the vector of movement, rotate instantly and skid turn
  1245. {
  1246. // todo: skid turn
  1247. }
  1248. else // otherwise, rotate slowly and move as normal
  1249. {
  1250. _RotateConstantToInputDirection(RotationSpeedSlow);
  1251. _AddForwardMovement();
  1252. }
  1253. return true;
  1254. }
  1255. else {
  1256. return false;
  1257. }
  1258. }
  1259.  
  1260.  
  1261. bool UPlayerMovementController::AirMovement()
  1262. {
  1263. if (Parent->IsApplyingInput())
  1264. {
  1265. _AddRelativeMovement();
  1266. _RotateConstantToInputDirection(RotationSpeedSlow);
  1267. //todo: does extra velocity need to be added?
  1268. return true;
  1269. }
  1270. else
  1271. {
  1272. return false;
  1273. }
  1274. }
  1275.  
  1276.  
  1277. void UPlayerMovementController::_AddForwardMovement()
  1278. {
  1279. FVector PlayerForwardVector = Parent->GetCapsuleComponent()->GetForwardVector();
  1280. FVector RawInputUnitVector;
  1281. float RawInputVectorLength;
  1282. Parent->GetRawInputVector().ToDirectionAndLength(OUT RawInputUnitVector, OUT RawInputVectorLength);
  1283. Parent->AddMovementInput(PlayerForwardVector, RawInputVectorLength, false);
  1284. }
  1285.  
  1286.  
  1287. void UPlayerMovementController::_AddRelativeMovement()
  1288. {
  1289. FRotator PlayerControlRotation = Parent->GetControlRotation();
  1290. FRotator NormalizedControlRotation{ 0.0, PlayerControlRotation.Yaw , 0.0f };
  1291. float HorizontalMovement = GetHorizontalAxisValue();
  1292. float VerticalMovement = GetVerticalAxisValue();
  1293. Parent->AddMovementInput(UKismetMathLibrary::GetRightVector(NormalizedControlRotation), HorizontalMovement, false);
  1294. Parent->AddMovementInput(UKismetMathLibrary::GetForwardVector(NormalizedControlRotation), VerticalMovement, false);
  1295. }
  1296.  
  1297.  
  1298. void UPlayerMovementController::_RotateConstantToInputDirection(float TurnSpeed)
  1299. {
  1300. FRotator NewRotationTarget = UKismetMathLibrary::RInterpTo_Constant(Parent->GetCapsuleComponent()->GetComponentRotation(),
  1301. InputRotationFromX, _Internal_DeltaTime, TurnSpeed);
  1302. Parent->GetCapsuleComponent()->SetWorldRotation(NewRotationTarget);
  1303. //UE_LOG(LogTemp, Warning, TEXT("Turning... %f"), _Internal_DeltaTime);
  1304. }
  1305.  
  1306.  
  1307. void UPlayerMovementController::_RotateInstantToInputDirection()
  1308. {
  1309. Parent->GetCapsuleComponent()->SetWorldRotation(InputRotationFromX);
  1310. }
  1311.  
  1312.  
  1313. float UPlayerMovementController::_GetAbsoluteRotationBetweenYaws(FRotator RotA, FRotator RotB)
  1314. {
  1315. float RawRotation = FMath::Abs(RotA.Yaw - RotB.Yaw);
  1316. if (RawRotation > 180.f)
  1317. {
  1318. return FMath::Abs(RawRotation - 360.f);
  1319. }
  1320. else
  1321. {
  1322. return RawRotation;
  1323. }
  1324. }
  1325.  
  1326.  
  1327. void UPlayerMovementController::_SetInputVectorAndRotation() // DEPRECATE
  1328. {
  1329. FRotator PlayerControlRotation = Parent->GetControlRotation();
  1330. FRotator NormalizedControlRotation{ 0.0f, PlayerControlRotation.Yaw, 0.0f };
  1331. float HorizontalMovement = GetHorizontalAxisValue();
  1332. float VerticalMovement = GetVerticalAxisValue();
  1333. FVector RawInput = (UKismetMathLibrary::GetRightVector(NormalizedControlRotation) * HorizontalMovement) +
  1334. (UKismetMathLibrary::GetForwardVector(NormalizedControlRotation) * VerticalMovement);
  1335. RawInputVector = RawInput;
  1336. RawInput.Normalize();
  1337. InputRotationFromX = UKismetMathLibrary::MakeRotFromX(RawInput);
  1338. }
  1339.  
  1340.  
  1341. void UPlayerMovementController::_CalculateInputRotationFromX()
  1342. {
  1343. InputRotationFromX = UKismetMathLibrary::MakeRotFromX(Parent->GetRawInputVector());
  1344. }
  1345.  
  1346.  
  1347. FVector UPlayerMovementController::GetRawInputVector()
  1348. {
  1349. return RawInputVector;
  1350. }
  1351.  
  1352.  
  1353. FRotator UPlayerMovementController::GetInputRotationFromX()
  1354. {
  1355. return InputRotationFromX;
  1356. }
  1357.  
  1358.  
  1359. void UPlayerMovementController::NewState(EMoveState NewMoveState)
  1360. {
  1361. (this->*StateFunction)(EStateInput::END);
  1362. //Parent->_SetMoveState(NewMoveState);
  1363. //SetMoveState(NewMoveState);
  1364. switch (NewMoveState) // Update with new states
  1365. {
  1366. case Idle:
  1367. StateFunction = &UPlayerMovementController::State_Idle;
  1368. break;
  1369. case Running:
  1370. StateFunction = &UPlayerMovementController::State_Run;
  1371. break;
  1372. case Jumping:
  1373. StateFunction = &UPlayerMovementController::State_Jump;
  1374. break;
  1375. case HighJumping:
  1376. StateFunction = &UPlayerMovementController::State_HighJump;
  1377. break;
  1378. case SpinJumping:
  1379. StateFunction = &UPlayerMovementController::State_SpinJump;
  1380. break;
  1381. case Falling:
  1382. StateFunction = &UPlayerMovementController::State_Fall;
  1383. break;
  1384. case Crouching:
  1385. StateFunction = &UPlayerMovementController::State_Crouch;
  1386. break;
  1387. case Crawling:
  1388. StateFunction = &UPlayerMovementController::State_Crawl;
  1389. break;
  1390. case Pounding:
  1391. StateFunction = &UPlayerMovementController::State_Pound;
  1392. break;
  1393. case PoundFlipping:
  1394. StateFunction = &UPlayerMovementController::State_PoundFlip;
  1395. break;
  1396. case Diving:
  1397. StateFunction = &UPlayerMovementController::State_Dive;
  1398. break;
  1399. case Dodging:
  1400. StateFunction = &UPlayerMovementController::State_Dodge;
  1401. break;
  1402. default:
  1403. break;
  1404. }
  1405.  
  1406. (this->*StateFunction)(EStateInput::INIT);
  1407. //_Debug_PrintState();
  1408. //(this->*StateFunction)(EStateInput::TICK); // todo: should this be called here?
  1409. }
  1410.  
  1411.  
  1412. void UPlayerMovementController::NotifyStateChanged_Implementation(EMoveState NewState)
  1413. {
  1414. // ...
  1415. }
  1416.  
  1417.  
  1418. void UPlayerMovementController::NotifySetChanged_Implementation(EMoveSet NewSet)
  1419. {
  1420. NewMoveSet_Notified(NewSet);
  1421. }
  1422.  
  1423.  
  1424. void UPlayerMovementController::NotifyOnLanded_Implementation()
  1425. {
  1426. (this->*StateFunction)(EStateInput::EVENT_Landed);
  1427. //Parent->JumpMaxCount = 1;
  1428. }
  1429.  
  1430.  
  1431. void UPlayerMovementController::NotifyOnJumped_Implementation()
  1432. {
  1433. (this->*StateFunction)(EStateInput::EVENT_Jumped);
  1434. }
  1435.  
  1436.  
  1437. void UPlayerMovementController::NotifyOnLaunched_Implementation()
  1438. {
  1439. (this->*StateFunction)(EStateInput::EVENT_Launched);
  1440. }
  1441.  
  1442.  
  1443. void UPlayerMovementController::NotifyHitMechanicFired_Implementation()
  1444. {
  1445. (this->*StateFunction)(EStateInput::EVENT_HitMechanic);
  1446. }
  1447.  
  1448.  
  1449. void UPlayerMovementController::NotifyCameraMechanicFired_Implementation()
  1450. {
  1451. (this->*StateFunction)(EStateInput::EVENT_CameraMechanic);
  1452. }
  1453.  
  1454.  
  1455. void UPlayerMovementController::NewState(void (UPlayerMovementController::* NewStateFunction)(EStateInput e))
  1456. {
  1457. (this->*StateFunction)(EStateInput::END);
  1458. StateFunction = NewStateFunction;
  1459. (this->*StateFunction)(EStateInput::INIT);
  1460. }
  1461.  
  1462.  
  1463. void UPlayerMovementController::CallStateFunction(EStateInput Input)
  1464. {
  1465. (this->*StateFunction)(Input);
  1466. }
  1467.  
  1468.  
  1469. void UPlayerMovementController::_Debug_PrintState()
  1470. {
  1471. switch (Parent->GetMoveState())
  1472. {
  1473. case Idle:
  1474. UE_LOG(LogTemp, Warning, TEXT("Current MoveState: Idle"));
  1475. break;
  1476. case Walking:
  1477. UE_LOG(LogTemp, Warning, TEXT("Current MoveState: Walking"));
  1478. break;
  1479. case Running:
  1480. UE_LOG(LogTemp, Warning, TEXT("Current MoveState: Running"));
  1481. break;
  1482. case Jumping:
  1483. UE_LOG(LogTemp, Warning, TEXT("Current MoveState: Jumping"));
  1484. break;
  1485. case HighJumping:
  1486. UE_LOG(LogTemp, Warning, TEXT("Current MoveState: HighJumping"));
  1487. break;
  1488. case SpinJumping:
  1489. UE_LOG(LogTemp, Warning, TEXT("Current MoveState: SpinJumping"));
  1490. break;
  1491. case Falling:
  1492. UE_LOG(LogTemp, Warning, TEXT("Current MoveState: Falling"));
  1493. break;
  1494. case Crouching:
  1495. UE_LOG(LogTemp, Warning, TEXT("Current MoveState: Crouching"));
  1496. break;
  1497. case Crawling:
  1498. UE_LOG(LogTemp, Warning, TEXT("Current MoveState: Crawling"));
  1499. break;
  1500. case Pounding:
  1501. UE_LOG(LogTemp, Warning, TEXT("Current MoveState: Pounding"));
  1502. break;
  1503. case PoundFlipping:
  1504. UE_LOG(LogTemp, Warning, TEXT("Current MoveState: PoundFlipping"));
  1505. break;
  1506. case Diving:
  1507. UE_LOG(LogTemp, Warning, TEXT("Current MoveState: Diving"));
  1508. break;
  1509. case Dodging:
  1510. UE_LOG(LogTemp, Warning, TEXT("Current MoveState: Dodging"));
  1511. break;
  1512. default:
  1513. break;
  1514. }
  1515. }
  1516.  
  1517.  
  1518. void UPlayerMovementController::NewMoveSet(EMoveSet NewMoveSet)
  1519. {
  1520. Parent->_SetMoveSet(NewMoveSet);
  1521. NewMoveSet_Notified(NewMoveSet);
  1522. }
  1523.  
  1524.  
  1525. void UPlayerMovementController::NewMoveSet_Notified(EMoveSet NewMoveSet)
  1526. {
  1527. (this->*StateFunction)(EStateInput::SET_END);
  1528. this->MoveSet = NewMoveSet;
  1529. (this->*StateFunction)(EStateInput::SET_INIT);
  1530. }
  1531.  
  1532.  
  1533. void UPlayerMovementController::Limited_Idle_Implementation(EStateInput input)
  1534. {
  1535. if (CommonActions(input)) { return; }
  1536.  
  1537. switch (input)
  1538. {
  1539. case INIT:
  1540. Parent->_SetMoveState(EMoveState::Idle);
  1541. break;
  1542.  
  1543. case TICK:
  1544. if (Ref_CharacterMovement->IsFalling())
  1545. {
  1546. NewState(EMoveState::Falling);
  1547. return;
  1548. }
  1549. else if (NormalMovement()) // If movement is being applied
  1550. {
  1551. NewState(EMoveState::Running);
  1552. return;
  1553. }
  1554. break;
  1555.  
  1556. case JUMP_Pressed:
  1557. NewState(EMoveState::Jumping);
  1558. return;
  1559.  
  1560. case CROUCH_Pressed:
  1561. NewState(EMoveState::Crouching);
  1562. return;
  1563.  
  1564. case HIT_Pressed:
  1565. // fire companion hit (prob in parent APlayer_Character)
  1566. break;
  1567.  
  1568. case END:
  1569.  
  1570. break;
  1571.  
  1572. default:
  1573. break;
  1574. }
  1575. }
  1576.  
  1577.  
  1578. void UPlayerMovementController::Limited_Run_Implementation(EStateInput input)
  1579. {
  1580. if (CommonActions(input)) { return; }
  1581.  
  1582. switch (input)
  1583. {
  1584. case INIT:
  1585. Parent->_SetMoveState(EMoveState::Running);
  1586. break;
  1587.  
  1588. case TICK:
  1589. if (Ref_CharacterMovement->IsFalling())
  1590. {
  1591. NewState(EMoveState::Falling);
  1592. return;
  1593. }
  1594. else if (!NormalMovement())
  1595. {
  1596. NewState(EMoveState::Idle);
  1597. return;
  1598. }
  1599. break;
  1600.  
  1601. case JUMP_Pressed:
  1602. NewState(EMoveState::Jumping);
  1603. return;
  1604.  
  1605. case CROUCH_Pressed:
  1606. NewState(EMoveState::Crawling);
  1607. break;
  1608.  
  1609. default:
  1610. break;
  1611. }
  1612. }
  1613.  
  1614.  
  1615. void UPlayerMovementController::Limited_Jump_Implementation(EStateInput input)
  1616. {
  1617. if (CommonOnLandedActions(input)) { return; }
  1618. if (_Common_Limited_Jump_Actions(input)) { return; }
  1619. //if (_RegularJump(input)) { return; }
  1620. if (_JumpTransitionGuard(input)) { return; }
  1621.  
  1622. switch (input)
  1623. {
  1624. case SET_INIT:
  1625. Ref_CharacterMovement->JumpZVelocity = 300.f;
  1626. break;
  1627. case INIT:
  1628. StandardJumpZVelocity = Ref_CharacterMovement->JumpZVelocity;
  1629. Ref_CharacterMovement->JumpZVelocity = 300.f;
  1630. Parent->Jump();
  1631. Parent->_SetMoveState(EMoveState::Jumping);
  1632. if (!Parent->CanJump())
  1633. {
  1634. NewState(EMoveState::Idle);
  1635. return;
  1636. }
  1637. break;
  1638.  
  1639. case TICK:
  1640. if (!Ref_CharacterMovement->IsFalling() && (!JumpInputBuffered || !Parent->CanJump())) //todo: can you refactor this to EVENT_Landed?
  1641. {
  1642. NewState(EMoveState::Idle);
  1643. return;
  1644. }
  1645. break;
  1646.  
  1647. case JUMP_Released:
  1648. Parent->StopJumping();
  1649. break;
  1650.  
  1651. case EVENT_Landed:
  1652. Parent->StopJumping();
  1653. break;
  1654.  
  1655. case END:
  1656. case SET_END:
  1657. Ref_CharacterMovement->JumpZVelocity = StandardJumpZVelocity;
  1658. break;
  1659.  
  1660. default:
  1661. break;
  1662. }
  1663. }
  1664.  
  1665.  
  1666. bool UPlayerMovementController::_Common_Limited_Jump_Actions(EStateInput input)
  1667. {
  1668. if (__JumpBuffer(input)) { return true; }
  1669. if (__JumpGravityScaling(input)) { return true; }
  1670. if (__StickyLanding(input)) { return true; }
  1671.  
  1672. switch (input)
  1673. {
  1674. case TICK:
  1675. AirMovement();
  1676. break;
  1677.  
  1678. case END:
  1679. //Parent->JumpMaxCount = 1;
  1680. //Parent->StopJumping();
  1681. break;
  1682.  
  1683. case FLASH_Pressed:
  1684. Parent->FireCameraMechanic();
  1685. break;
  1686.  
  1687. default:
  1688. break;
  1689. }
  1690. return false;
  1691. }
  1692.  
  1693.  
  1694. void UPlayerMovementController::Limited_Fall_Implementation(EStateInput input)
  1695. {
  1696. if (CommonOnLandedActions(input)) { return; }
  1697. if (_Common_Limited_Jump_Actions(input)) { return; }
  1698. if (_CoyoteTime(input)) { return; }
  1699.  
  1700. switch (input)
  1701. {
  1702. case INIT:
  1703. Parent->_SetMoveState(EMoveState::Falling);
  1704. break;
  1705.  
  1706. case TICK:
  1707. if (!Ref_CharacterMovement->IsFalling())
  1708. {
  1709. NewState(EMoveState::Idle);
  1710. return;
  1711. }
  1712. break;
  1713.  
  1714. case END:
  1715.  
  1716. break;
  1717.  
  1718. default:
  1719. break;
  1720. }
  1721. }
  1722.  
  1723.  
  1724. void UPlayerMovementController::Limited_Crouch_Implementation(EStateInput input)
  1725. {
  1726. if (_CommonActions_Limited(input)) { return; }
  1727.  
  1728. switch (input)
  1729. {
  1730. case INIT:
  1731. Parent->_SetMoveState(EMoveState::Crouching);
  1732. Parent->Crouch();
  1733. break;
  1734.  
  1735. case TICK:
  1736. if (NormalMovement())
  1737. {
  1738. NewState(EMoveState::Crawling);
  1739. return;
  1740. }
  1741. break;
  1742.  
  1743. case CROUCH_Released:
  1744. Parent->UnCrouch();
  1745. NewState(EMoveState::Idle);
  1746. return;
  1747.  
  1748. case END:
  1749. //Parent->UnCrouch(); //todo: what if transitioning to crawl? does uncrouch, then crouch work?
  1750. break;
  1751.  
  1752. case RESET:
  1753. Parent->UnCrouch();
  1754. break;
  1755.  
  1756. default:
  1757. break;
  1758. }
  1759. }
  1760.  
  1761.  
  1762. bool UPlayerMovementController::_CommonActions_Limited(EStateInput input)
  1763. {
  1764. switch (input)
  1765. {
  1766. case FLASH_Pressed:
  1767. Parent->FireCameraMechanic();
  1768. break;
  1769.  
  1770. default:
  1771. break;
  1772. }
  1773. return false;
  1774. }
  1775.  
  1776.  
  1777. void UPlayerMovementController::Limited_Crawl_Implementation(EStateInput input)
  1778. {
  1779. if (_CommonActions_Limited(input)) { return; }
  1780.  
  1781. switch (input)
  1782. {
  1783. case INIT:
  1784. Parent->_SetMoveState(EMoveState::Crawling);
  1785. Parent->Crouch();
  1786. break;
  1787.  
  1788. case TICK:
  1789. if (!NormalMovement())
  1790. {
  1791. NewState(EMoveState::Crouching);
  1792. return;
  1793. }
  1794. break;
  1795.  
  1796. case CROUCH_Released:
  1797. Parent->UnCrouch();
  1798. NewState(EMoveState::Running);
  1799. break;
  1800.  
  1801. case END:
  1802. //Parent->UnCrouch();
  1803. break;
  1804.  
  1805. case RESET:
  1806. Parent->UnCrouch();
  1807. break;
  1808.  
  1809. default:
  1810. break;
  1811. }
  1812. }
  1813.  

Iterative Playtesting

Methodology

The only way to “find the fun” and know that a game mechanic or level idea is successful is to put it in front of players and test it.

I implemented an iterative testing cycle alongside our user experience team to capture player feedback continuously throughout development, iterating as we went.

Playtest Process

Playtest feedback is aggregated on sticky notes using Miro. This lets us organize the feedback we have received and take notes, showing us at a glance where trouble spots or problem mechanics are.

From here, we can build a priority queue of changes to improve on for the next iteration.

All feedback from each iteration is pooled together on one virtual board, so we can see how the mechanics and levels are improving from week to week, and specifically note regressions or observe trends.

Blueprint/C++ Integration

A Combined Approach

C++ and Blueprints work together  to enable fast iteration and high performance. Our early prototypes began as Blueprint code and were later refactored to C++ to optimize for performance.

Integrating C++ and Blueprints

All gameplay objects are children of base C++ classes.

Unreal C++ macros are used to demarcate functions and variables that should be exposed to Blueprint code in the child classes.

This allows us to write custom Blueprint nodes in C++, and then use them in the Blueprint node graph to quickly create prototypes or new functionality.

Ensuring gameplay objects are Blueprint children also enables us to utilize Unreal features that are faster in Blueprint, such as editing an object”s components or using asynchronous processes such as Timelines.

A combined approach saves time and effort for programmers and designers, while maintaining high performance.

Character Finite State Machine

Performance and Extensibility

A finite state system provides optimized performance for the player input system. Using discrete states and function pointers, we can isolate functionality and extend it as needed while saving conditional checks.

 

 

Representing States via Enums

The enumerator EMoveState defines the discrete movement states the player may be in at any given time.

Then, the actual behavior is defined by a MoveSet. The enum EMoveSet defines the actual movesets, and can be expanded to as needed. These movesets point the function pointers to the state functions for the given MoveSet.

In this example, we have a Standard moveset that the player typically uses. At certain times, we want to limit a player’s actions. For that, we use the “Limited” moveset and define in code a different set of state functions for what “Limited” movement looks like.

Handling Input

All player and engine input is routed through a custom input handling function. The enum EStateInput defines each input, and this input is passed to each state function. The state function then handles behavior or transitions based on the input given.

Inputs include buttons the player presses, as well as engine events such as the event tick.

UENUM(BlueprintType)
enum EMoveState
{
	Idle UMETA(DisplayName = "Idle"),
	Walking UMETA(DisplayName = "Walking"),
	Running UMETA(DisplayName = "Running"), 
	Jumping UMETA(DisplayName = "Jumping"),
	HighJumping UMETA(DisplayName = "HighJumping"),
	SpinJumping UMETA(DisplayName = "SpinJumping"),
	Falling UMETA(DisplayName = "Falling"),
	Crouching UMETA(DisplayName = "Crouching"),
	Crawling UMETA(DisplayName = "Crawling"),
	Pounding UMETA(DisplayName = "Pounding"),
	PoundFlipping UMETA(DisplayName = "PoundFlipping"),
	Diving UMETA(DisplayName = "Diving"),
	Dodging UMETA(DisplayName = "Dodging"),
};

UENUM(BlueprintType)
enum EMoveSet
{
	Standard UMETA(DisplayName = "Standard"),
	Limited UMETA(DisplayName = "Limited"),
};
/**
 * This represents all possible inputs for each  
 * movement state that the player can be in.
 */
UENUM(BlueprintType)
enum EStateInput
{
	TICK,
	INIT,
	END,
	SET_INIT,
	SET_END,
	JUMP_Pressed,
	JUMP_Released,
	CROUCH_Pressed,
	CROUCH_Released,
	FLASH_Pressed,
	FLASH_Released,
	GRAB_Pressed,
	GRAB_Released,
	HIT_Pressed,
	HIT_Released,
	AXIS,
	AXIS_HORIZONTAL,
	AXIS_VERTICAL,
	EVENT_Landed,
	EVENT_Jumped,
	EVENT_Launched,
	EVENT_HitMechanic,
	EVENT_CameraMechanic,
	TIMER_PoundFlip,
	RESET,
};

Minimize Clock Cycles with Delegates and Function Pointers

Input and gameplay logic is handled in an event-based manner. This minimizes the need for costly functions to run in the frame tick.

Player movement logic is kept in atomic handler functions, and when the player’s moveset changes, a function delegate is given a pointer to the next handler function. That function then receives player input and engine frame ticks, preventing the need for extraneous if statements or state checks.

// Jump input is abstracted and passed as a state transition into the state machine.

void UPlayerMovementController::JumpActionPressed()
{
	if (!InputEnabled) { return; }
	(this->*StateFunction)(EStateInput::JUMP_Pressed);
}
// Designed to be extended with new movesets.

void UPlayerMovementController::State_Jump_Implementation(EStateInput input)
{
	switch (MoveSet)
	{
	case Limited:
		Limited_Jump(input);
		break;

	default:
		Standard_Jump(input);
		break;
	}
}
  1. // Copyright 2022 Soup Initiative Games
  2.  
  3.  
  4. #include "ProjectSpooky64/Public/Pawns/PlayerMovementController.h"
  5. #include "ProjectSpooky64/Public/Pawns/APlayer_Character.h"
  6. #include "PaperCharacter.h"
  7. #include "Components/CapsuleComponent.h"
  8. #include "GameFramework/Actor.h"
  9. #include "Kismet/KismetMathLibrary.h"
  10. #include "Kismet/GameplayStatics.h"
  11.  
  12.  
  13. // Sets default values for this component's properties
  14. UPlayerMovementController::UPlayerMovementController()
  15. {
  16. // Set this component to be initialized when the game starts, and to be ticked every frame. You can turn these features
  17. // off to improve performance if you don't need them.
  18. PrimaryComponentTick.bCanEverTick = true;
  19.  
  20. // ...
  21. MoveState = EMoveState::Idle;
  22. MoveSet = EMoveSet::Standard;
  23. }
  24.  
  25.  
  26. // Called when the game starts
  27. void UPlayerMovementController::BeginPlay()
  28. {
  29. Super::BeginPlay();
  30.  
  31. //Ref_CharacterMovement = GetOwner()->FindComponentByClass<UCharacterMovementComponent>();
  32. Parent = Cast<AAPlayer_Character>(GetOwner());
  33. Ref_CharacterMovement = Parent->GetCharacterMovement();
  34.  
  35. Parent->JumpMaxHoldTime = JumpBufferMaxTime;
  36.  
  37. Parent->OnMoveStateChanged.AddDynamic(this, &UPlayerMovementController::NotifyStateChanged);
  38. Parent->OnMoveSetChanged.AddDynamic(this, &UPlayerMovementController::NotifySetChanged);
  39. Parent->OnLanded.AddDynamic(this, &UPlayerMovementController::NotifyOnLanded);
  40. Parent->OnJumped.AddDynamic(this, &UPlayerMovementController::NotifyOnJumped);
  41. Parent->OnLaunched.AddDynamic(this, &UPlayerMovementController::NotifyOnLaunched);
  42. Parent->HitMechanic.AddDynamic(this, &UPlayerMovementController::NotifyHitMechanicFired);
  43. Parent->CameraMechanic.AddDynamic(this, &UPlayerMovementController::NotifyCameraMechanicFired);
  44.  
  45. StateFunction = &UPlayerMovementController::State_Idle;
  46. (this->*StateFunction)(EStateInput::INIT);
  47.  
  48.  
  49. }
  50.  
  51.  
  52. // Called every frame
  53. void UPlayerMovementController::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
  54. {
  55. Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
  56. //UE_LOG(LogTemp, Warning, TEXT("Jump buffered: %s"), (JumpInputBuffered) ? TEXT("TRUE") : TEXT("false"));
  57. }
  58.  
  59.  
  60. void UPlayerMovementController::GuardedTick_Implementation(float DeltaTime)
  61. {
  62. _Internal_DeltaTime = DeltaTime;
  63.  
  64. //_SetInputVectorAndRotation(); // todo: might be able to move this to a lower function later
  65.  
  66. _CalculateInputRotationFromX();
  67.  
  68. (this->*StateFunction)(EStateInput::TICK);
  69.  
  70. //UE_LOG(LogTemp, Warning, TEXT("Can the parent jump: %s"), (Parent->CanJump() ? TEXT("true") : TEXT("false")));
  71.  
  72. //_Debug_PrintState();
  73. }
  74.  
  75.  
  76. float UPlayerMovementController::GetInternalDeltaTime()
  77. {
  78. return _Internal_DeltaTime;
  79. }
  80.  
  81.  
  82. void UPlayerMovementController::SetMoveState(EMoveState NewState)
  83. {
  84. MoveState = NewState;
  85. }
  86.  
  87.  
  88. void UPlayerMovementController::SetMoveSet(EMoveSet NewSet)
  89. {
  90. MoveSet = NewSet;
  91. }
  92.  
  93.  
  94. EMoveState UPlayerMovementController::GetMoveState()
  95. {
  96. return MoveState.GetValue();
  97. }
  98.  
  99.  
  100. EMoveSet UPlayerMovementController::GetMoveSet()
  101. {
  102. return MoveSet.GetValue();
  103. }
  104.  
  105.  
  106. void UPlayerMovementController::InitializeInputs(UInputComponent* PlayerInputComponent)
  107. {
  108. UE_LOG(LogTemp, Warning, TEXT("PlayerMovementController initialized."));
  109. PlayerInputComponent->BindAction("Jump", EInputEvent::IE_Pressed, this, &UPlayerMovementController::JumpActionPressed);
  110. PlayerInputComponent->BindAction("Jump", EInputEvent::IE_Released, this, &UPlayerMovementController::JumpActionReleased);
  111. PlayerInputComponent->BindAction("Crouch", EInputEvent::IE_Pressed, this, &UPlayerMovementController::CrouchActionPressed);
  112. PlayerInputComponent->BindAction("Crouch", EInputEvent::IE_Released, this, &UPlayerMovementController::CrouchActionReleased);
  113. PlayerInputComponent->BindAction("Flash", EInputEvent::IE_Pressed, this, &UPlayerMovementController::FlashActionPressed);
  114. PlayerInputComponent->BindAction("Flash", EInputEvent::IE_Released, this, &UPlayerMovementController::FlashActionReleased);
  115. PlayerInputComponent->BindAction("Grab", EInputEvent::IE_Pressed, this, &UPlayerMovementController::GrabActionPressed);
  116. PlayerInputComponent->BindAction("Grab", EInputEvent::IE_Released, this, &UPlayerMovementController::GrabActionReleased);
  117. PlayerInputComponent->BindAction("Hit", EInputEvent::IE_Pressed, this, &UPlayerMovementController::HitActionPressed);
  118. PlayerInputComponent->BindAction("Hit", EInputEvent::IE_Released, this, &UPlayerMovementController::HitActionReleased);
  119. PlayerInputComponent->BindAxis("HorizontalMovement", this, &UPlayerMovementController::HorizontalAxisInput);
  120. PlayerInputComponent->BindAxis("VerticalMovement", this, &UPlayerMovementController::VerticalAxisInput);
  121. }
  122.  
  123.  
  124. void UPlayerMovementController::JumpActionPressed()
  125. {
  126. if (!InputEnabled) { return; }
  127. (this->*StateFunction)(EStateInput::JUMP_Pressed);
  128. }
  129.  
  130.  
  131. void UPlayerMovementController::JumpActionReleased()
  132. {
  133. if (!InputEnabled) { return; }
  134. (this->*StateFunction)(EStateInput::JUMP_Released);
  135. }
  136.  
  137.  
  138. void UPlayerMovementController::CrouchActionPressed()
  139. {
  140. if (!InputEnabled) { return; }
  141. (this->*StateFunction)(EStateInput::CROUCH_Pressed);
  142. //UE_LOG(LogTemp, Warning, TEXT("Crouch action pressed"));
  143. }
  144.  
  145.  
  146. void UPlayerMovementController::CrouchActionReleased()
  147. {
  148. if (!InputEnabled) { return; }
  149. (this->*StateFunction)(EStateInput::CROUCH_Released);
  150. }
  151.  
  152.  
  153. void UPlayerMovementController::FlashActionPressed()
  154. {
  155. if (!InputEnabled) { return; }
  156. (this->*StateFunction)(EStateInput::FLASH_Pressed);
  157. }
  158.  
  159.  
  160. void UPlayerMovementController::FlashActionReleased()
  161. {
  162. if (!InputEnabled) { return; }
  163. (this->*StateFunction)(EStateInput::FLASH_Released);
  164. Parent->ReleaseCameraMechanic();
  165. }
  166.  
  167.  
  168. void UPlayerMovementController::GrabActionPressed()
  169. {
  170. if (!InputEnabled) { return; }
  171. (this->*StateFunction)(EStateInput::GRAB_Pressed);
  172. Parent->FireAimMechanic(); // Might want to refactor into a state helper function.
  173. UE_LOG(LogTemp, Warning, TEXT("Grab action pressed"));
  174. }
  175.  
  176.  
  177. void UPlayerMovementController::GrabActionReleased()
  178. {
  179. if (!InputEnabled) { return; }
  180. (this->*StateFunction)(EStateInput::GRAB_Released);
  181. Parent->ReleaseAimMechanic(); // Might want to refactor into a state helper function.
  182. UE_LOG(LogTemp, Warning, TEXT("Grab action released"));
  183. }
  184.  
  185.  
  186. void UPlayerMovementController::HitActionPressed()
  187. {
  188. if (!InputEnabled) { return; }
  189. (this->*StateFunction)(EStateInput::HIT_Pressed);
  190. }
  191.  
  192.  
  193. void UPlayerMovementController::HitActionReleased()
  194. {
  195. if (!InputEnabled) { return; }
  196. (this->*StateFunction)(EStateInput::HIT_Released);
  197. Parent->ReleaseHitMechanic();
  198. }
  199.  
  200.  
  201. void UPlayerMovementController::HorizontalAxisInput(float AxisValue)
  202. {
  203.  
  204. }
  205.  
  206.  
  207. void UPlayerMovementController::VerticalAxisInput(float AxisValue)
  208. {
  209.  
  210. }
  211.  
  212.  
  213. void UPlayerMovementController::SetMoveEnabled(bool Enabled)
  214. {
  215. if (Enabled)
  216. {
  217. this->InputEnabled = true;
  218. Parent->_SetMovementInputEnabled(true);
  219. }
  220. else
  221. {
  222. this->InputEnabled = false;
  223. Parent->_SetMovementInputEnabled(false);
  224. ResetMovement();
  225. }
  226. }
  227.  
  228.  
  229. float UPlayerMovementController::GetHorizontalAxisValue()
  230. {
  231. if (!InputEnabled) { return 0.f; }
  232. return Parent->GetInputAxisValue("HorizontalMovement");
  233. }
  234.  
  235. float UPlayerMovementController::GetVerticalAxisValue()
  236. {
  237. if (!InputEnabled) { return 0.f; }
  238. return Parent->GetInputAxisValue("VerticalMovement");
  239. }
  240.  
  241.  
  242. void UPlayerMovementController::ResetMovement()
  243. {
  244. (this->*StateFunction)(EStateInput::RESET);
  245. NewState(EMoveState::Idle);
  246. Ref_CharacterMovement->Velocity = FVector{ 0.f, 0.f, 0.f };
  247. }
  248.  
  249.  
  250. void UPlayerMovementController::State_Idle_Implementation(EStateInput input)
  251. {
  252. switch(MoveSet)
  253. {
  254. case Limited:
  255. Limited_Idle(input);
  256. break;
  257.  
  258. default:
  259. Standard_Idle(input);
  260. break;
  261. }
  262. }
  263.  
  264.  
  265. void UPlayerMovementController::State_Run_Implementation(EStateInput input)
  266. {
  267. switch (MoveSet)
  268. {
  269. case Limited:
  270. Limited_Run(input);
  271. break;
  272.  
  273. default:
  274. Standard_Run(input);
  275. break;
  276. }
  277. }
  278.  
  279.  
  280. void UPlayerMovementController::State_Jump_Implementation(EStateInput input)
  281. {
  282. switch (MoveSet)
  283. {
  284. case Limited:
  285. Limited_Jump(input);
  286. break;
  287.  
  288. default:
  289. Standard_Jump(input);
  290. break;
  291. }
  292. }
  293.  
  294.  
  295. void UPlayerMovementController::State_HighJump_Implementation(EStateInput input)
  296. {
  297. switch (MoveSet)
  298. {
  299. case Limited:
  300. break;
  301.  
  302. default:
  303. Standard_HighJump(input);
  304. break;
  305. }
  306. }
  307.  
  308.  
  309. void UPlayerMovementController::State_SpinJump_Implementation(EStateInput input)
  310. {
  311. switch (MoveSet)
  312. {
  313. case Limited:
  314. break;
  315.  
  316. default:
  317. Standard_SpinJump(input);
  318. break;
  319. }
  320. }
  321.  
  322.  
  323. void UPlayerMovementController::State_Fall_Implementation(EStateInput input)
  324. {
  325. switch (MoveSet)
  326. {
  327. case Limited:
  328. Limited_Fall(input);
  329. break;
  330.  
  331. default:
  332. Standard_Fall(input);
  333. break;
  334. }
  335. }
  336.  
  337.  
  338. void UPlayerMovementController::State_Crouch_Implementation(EStateInput input)
  339. {
  340. switch (MoveSet)
  341. {
  342. case Limited:
  343. Limited_Crouch(input);
  344. break;
  345.  
  346. default:
  347. Standard_Crouch(input);
  348. break;
  349. }
  350. }
  351.  
  352.  
  353. void UPlayerMovementController::State_Crawl_Implementation(EStateInput input)
  354. {
  355. switch (MoveSet)
  356. {
  357. case Limited:
  358. Limited_Crawl(input);
  359. break;
  360.  
  361. default:
  362. Standard_Crawl(input);
  363. break;
  364. }
  365. }
  366.  
  367.  
  368. void UPlayerMovementController::State_Pound_Implementation(EStateInput input)
  369. {
  370. switch (MoveSet)
  371. {
  372. case Limited:
  373. break;
  374.  
  375. default:
  376. Standard_Pound(input);
  377. break;
  378. }
  379. }
  380.  
  381.  
  382. void UPlayerMovementController::State_PoundFlip_Implementation(EStateInput input)
  383. {
  384. switch (MoveSet)
  385. {
  386. case Limited:
  387. break;
  388.  
  389. default:
  390. Standard_PoundFlip(input);
  391. break;
  392. }
  393. }
  394.  
  395.  
  396. void UPlayerMovementController::State_Dive_Implementation(EStateInput input)
  397. {
  398. switch (MoveSet)
  399. {
  400. case Limited:
  401. break;
  402.  
  403. default:
  404. Standard_Dive(input);
  405. break;
  406. }
  407. }
  408.  
  409.  
  410. void UPlayerMovementController::State_Dodge_Implementation(EStateInput input)
  411. {
  412. switch (MoveSet)
  413. {
  414. case Limited:
  415. break;
  416.  
  417. default:
  418. Standard_Dodge(input);
  419. break;
  420. }
  421. }
  422.  
  423.  
  424. /*
  425.  * Note:
  426.  * When extending in Blueprint,
  427.  * call Super/parent version for every case that isn't going to have a unique Blueprint implementation.
  428.  */
  429. void UPlayerMovementController::Standard_Idle_Implementation(EStateInput input)
  430. {
  431. if (CommonActions(input)) { return; }
  432.  
  433. switch (input)
  434. {
  435. case INIT:
  436. Parent->_SetMoveState(EMoveState::Idle);
  437. break;
  438.  
  439. case TICK:
  440. if (Ref_CharacterMovement->IsFalling())
  441. {
  442. NewState(EMoveState::Falling);
  443. return;
  444. }
  445. else if (NormalMovement()) // If movement is being applied
  446. {
  447. NewState(EMoveState::Running);
  448. return;
  449. }
  450. break;
  451.  
  452. case JUMP_Pressed:
  453. NewState(EMoveState::Jumping);
  454. return;
  455.  
  456. case CROUCH_Pressed:
  457. NewState(EMoveState::Crouching);
  458. return;
  459.  
  460. case HIT_Pressed:
  461. // fire companion hit (prob in parent APlayer_Character)
  462. break;
  463.  
  464. case END:
  465.  
  466. break;
  467.  
  468. default:
  469. break;
  470. }
  471. }
  472.  
  473.  
  474. void UPlayerMovementController::Standard_Run_Implementation(EStateInput input)
  475. {
  476. if (CommonActions(input)) { return; }
  477.  
  478. switch (input)
  479. {
  480. case INIT:
  481. Parent->_SetMoveState(EMoveState::Running);
  482. break;
  483.  
  484. case TICK:
  485. if (Ref_CharacterMovement->IsFalling())
  486. {
  487. NewState(EMoveState::Falling);
  488. return;
  489. }
  490. else if (!NormalMovement())
  491. {
  492. NewState(EMoveState::Idle);
  493. return;
  494. }
  495. break;
  496.  
  497. case JUMP_Pressed:
  498. NewState(EMoveState::Jumping);
  499. return;
  500.  
  501. case CROUCH_Pressed:
  502. NewState(EMoveState::Crawling);
  503. break;
  504.  
  505. default:
  506. break;
  507. }
  508. }
  509.  
  510.  
  511. void UPlayerMovementController::Standard_Jump_Implementation(EStateInput input)
  512. {
  513. if (CommonOnLandedActions(input)) { return; }
  514.  
  515. /*
  516. * Returns true if NewState is called in CommonJumpAction's scope, therefore preventing the remainder of this function
  517. * from firing after the state has been changed.
  518. */
  519. if (CommonJumpActions(input)) { return; }
  520.  
  521. if (_RegularJump(input)) { return; }
  522. if (_JumpTransitionGuard(input)) { return; }
  523.  
  524. switch (input)
  525. {
  526. case INIT:
  527. Parent->_SetMoveState(EMoveState::Jumping);
  528. //UE_LOG(LogTemp, Warning, TEXT("Jump max count: %i"), Parent->JumpMaxCount);
  529. //Parent->Jump();
  530. //CharMovement_JumpBuffered = true; // Guard for notifying TICK case that there is a jump waiting to be applied next update
  531. if (!Parent->CanJump())
  532. {
  533. NewState(EMoveState::Idle);
  534. return;
  535. }
  536. break;
  537.  
  538. case TICK:
  539. //UE_LOG(LogTemp, Warning, TEXT("Jump buffered: %s"), (JumpInputBuffered) ? TEXT("TRUE") : TEXT("false"));
  540. if (!Ref_CharacterMovement->IsFalling() && (!JumpInputBuffered || !Parent->CanJump())) //todo: can you refactor this to EVENT_Landed?
  541. {
  542. NewState(EMoveState::Idle);
  543. return;
  544. }
  545. /*todo: if jump is called but cannot be processed, call idle*/
  546. //CharMovement_JumpBuffered = false;
  547. break;
  548.  
  549. case JUMP_Released:
  550. Parent->StopJumping();
  551. break;
  552.  
  553. case CROUCH_Released:
  554.  
  555. break;
  556.  
  557. case EVENT_Landed:
  558. Parent->StopJumping();
  559. break;
  560.  
  561. case EVENT_Jumped:
  562. //CharMovement_JumpBuffered = false;
  563. break;
  564.  
  565. case END:
  566. //CharMovement_JumpBuffered = true;
  567. //Parent->JumpMaxCount = 1;
  568. break;
  569.  
  570. default:
  571. break;
  572. }
  573. }
  574.  
  575.  
  576. bool UPlayerMovementController::_RegularJump(EStateInput input)
  577. {
  578. switch (input)
  579. {
  580. case INIT:
  581. Parent->Jump();
  582. break;
  583.  
  584. case END:
  585. Parent->JumpMaxCount = 1;
  586. break;
  587.  
  588. default:
  589. break;
  590. }
  591. return false;
  592. }
  593.  
  594.  
  595. bool UPlayerMovementController::CommonOnLandedActions_Implementation(EStateInput input)
  596. {
  597. switch (input)
  598. {
  599. case RESET:
  600. case EVENT_Landed:
  601. SpinJumpsAttempted = 0;
  602. DivesAttempted = 0;
  603. Parent->StopJumping();
  604. break;
  605.  
  606. default:
  607. break;
  608. }
  609. return false;
  610. }
  611.  
  612.  
  613. void UPlayerMovementController::Standard_HighJump_Implementation(EStateInput input)
  614. {
  615. if (CommonOnLandedActions(input)) { return; }
  616. if (CommonJumpActions(input)) { return; }
  617.  
  618. switch (input)
  619. {
  620. case INIT:
  621. Parent->_SetMoveState(EMoveState::HighJumping);
  622. Parent->LaunchCharacter(FVector(0.f, 0.f, Ref_CharacterMovement->JumpZVelocity * 1.75), true, true);
  623. break;
  624.  
  625. case TICK:
  626. break;
  627.  
  628. case EVENT_Landed: //todo: is this going to cause errors? revisit if errors occur around highjumping
  629. NewState(EMoveState::Idle);
  630. return;
  631.  
  632. default:
  633. break;
  634. }
  635. }
  636.  
  637.  
  638. void UPlayerMovementController::Standard_SpinJump_Implementation(EStateInput input)
  639. {
  640. if (CommonOnLandedActions(input)) { return; }
  641.  
  642. switch (input)
  643. {
  644. case INIT:
  645. {
  646. SpinJumpsAttempted++;
  647. Parent->_SetMoveState(EMoveState::SpinJumping);
  648. __DefaultGravityScalePeak = GravityScalePeak; // Note: Alters state variables here, which are reset once this state ends.
  649. GravityScalePeak = GravityScalePeak / 2.f;
  650. FVector _Velocity = Ref_CharacterMovement->Velocity;
  651. FVector2D F2D_Velocity{ _Velocity.X, _Velocity.Y };
  652. /*
  653. FVector InputVector = InputRotationFromX.Vector(); // get input from player forward vector
  654. FVector2D F2D_InputVector{ InputVector.X, InputVector.Y }; // make 2D
  655. F2D_Velocity = F2D_Velocity * F2D_InputVector; // @todo: this should redirect velocity in direction of movement
  656. */
  657. FVector2D F2D_VelocityDirection{ 0.f };
  658. float F2D_VelocityMagnitude = 0.f; // we want the magnitude
  659. F2D_Velocity.ToDirectionAndLength(F2D_VelocityDirection, F2D_VelocityMagnitude);
  660. FVector2D F2D_InputVector{ RawInputVector.X, RawInputVector.Y }; // get raw input vector 2D
  661.  
  662. FVector InputVect = InputRotationFromX.Vector();
  663. F2D_Velocity.X = InputVect.X;
  664. F2D_Velocity.Y = InputVect.Y;
  665.  
  666. if (Parent->IsApplyingInput()) // Invalidate X and Y velocity if applying input. Used to make maneuvering easier.
  667. {
  668. /*
  669. F2D_Velocity.X = 0.f;
  670. F2D_Velocity.Y = 0.f;
  671. */
  672. //F2D_Velocity = F2D_InputVector * F2D_VelocityMagnitude; // set velocity in the direction of input
  673. F2D_Velocity = F2D_Velocity * SpinJumpHorizontalVelocity;
  674. }
  675. //F2D_Velocity = F2D_Velocity * 0.65; // dampen velocity
  676. Parent->LaunchCharacter({ F2D_Velocity.X, F2D_Velocity.Y, SpinJumpVerticalVelocity }, true, true);
  677. break;
  678. }
  679.  
  680. case CROUCH_Pressed:
  681. NewState(EMoveState::PoundFlipping);
  682. return;
  683.  
  684. default:
  685. break;
  686. }
  687.  
  688. if (CommonJumpActions(input)) { return; }
  689. // if (_SpinJumpGravityScaling(input)) { return; }
  690.  
  691. switch (input)
  692. {
  693. case EVENT_Landed:
  694. NewState(EMoveState::Idle);
  695. break;
  696.  
  697. case END:
  698. GravityScalePeak = __DefaultGravityScalePeak;
  699. break;
  700.  
  701. default:
  702. break;
  703. }
  704. }
  705.  
  706.  
  707. bool UPlayerMovementController::_SpinJumpGravityScaling(EStateInput input)
  708. {
  709. switch (input)
  710. {
  711. case TICK:
  712. {
  713. FVector CapsuleVelocity = Parent->GetCapsuleComponent()->GetComponentVelocity();
  714. if ((CapsuleVelocity.Z < JumpVelocityUpThreshold) && (CapsuleVelocity.Z > JumpVelocityDownTreshold))
  715. {
  716. Ref_CharacterMovement->GravityScale = GravityScalePeak / 2.f;
  717. }
  718. break;
  719. }
  720.  
  721. default:
  722. break;
  723. }
  724. return false;
  725. }
  726.  
  727.  
  728. void UPlayerMovementController::Standard_Fall_Implementation(EStateInput input)
  729. {
  730. if (CommonOnLandedActions(input)) { return; }
  731. if (CommonJumpActions(input)) { return; }
  732. if (_CoyoteTime(input)) { return; }
  733.  
  734. switch (input)
  735. {
  736. case INIT:
  737. Parent->_SetMoveState(EMoveState::Falling);
  738.  
  739. break;
  740.  
  741. case TICK:
  742. if (!Ref_CharacterMovement->IsFalling())
  743. {
  744. NewState(EMoveState::Idle);
  745. return;
  746. }
  747. break;
  748.  
  749. case END:
  750.  
  751. break;
  752.  
  753. default:
  754. break;
  755. }
  756. }
  757.  
  758.  
  759. bool UPlayerMovementController::_CoyoteTime(EStateInput input)
  760. {
  761. switch (input)
  762. {
  763. case TICK:
  764. _CoyoteTimeAccumulation += _Internal_DeltaTime;
  765. break;
  766.  
  767. case JUMP_Pressed:
  768. if (_CoyoteTimeAccumulation < CoyoteTimeDuration)
  769. {
  770. Parent->JumpMaxCount = 2;
  771. NewState(EMoveState::Jumping);
  772. return true;
  773. }
  774. break;
  775.  
  776. case END:
  777. _CoyoteTimeAccumulation = 0.f;
  778. break;
  779.  
  780. default:
  781. break;
  782. }
  783. return false;
  784. }
  785.  
  786.  
  787. void UPlayerMovementController::Standard_Crouch_Implementation(EStateInput input)
  788. {
  789. if (CommonActions(input)) { return; }
  790.  
  791. switch (input)
  792. {
  793. case INIT:
  794. Parent->_SetMoveState(EMoveState::Crouching);
  795. Parent->Crouch();
  796. break;
  797.  
  798. case TICK:
  799. if (NormalMovement())
  800. {
  801. NewState(EMoveState::Crawling);
  802. return;
  803. }
  804. break;
  805.  
  806. case CROUCH_Released:
  807. Parent->UnCrouch();
  808. NewState(EMoveState::Idle);
  809. return;
  810.  
  811. case JUMP_Pressed:
  812. Parent->UnCrouch();
  813. NewState(EMoveState::HighJumping);
  814. return;
  815.  
  816. case END:
  817. //Parent->UnCrouch(); //todo: what if transitioning to crawl? does uncrouch, then crouch work?
  818. break;
  819.  
  820. case RESET:
  821. Parent->UnCrouch();
  822. break;
  823.  
  824. default:
  825. break;
  826. }
  827. }
  828.  
  829.  
  830. void UPlayerMovementController::Standard_Crawl_Implementation(EStateInput input)
  831. {
  832. if (CommonActions(input)) { return; }
  833.  
  834. switch (input)
  835. {
  836. case INIT:
  837. Parent->_SetMoveState(EMoveState::Crawling);
  838. Parent->Crouch();
  839. break;
  840.  
  841. case TICK:
  842. if (!NormalMovement())
  843. {
  844. NewState(EMoveState::Crouching);
  845. return;
  846. }
  847. break;
  848.  
  849. case CROUCH_Released:
  850. Parent->UnCrouch();
  851. NewState(EMoveState::Running);
  852. break;
  853.  
  854. case JUMP_Pressed:
  855. Parent->UnCrouch();
  856. NewState(EMoveState::Dodging);
  857. return;
  858.  
  859. case END:
  860. //Parent->UnCrouch();
  861. break;
  862.  
  863. case RESET:
  864. Parent->UnCrouch();
  865. break;
  866.  
  867. default:
  868. break;
  869. }
  870. }
  871.  
  872.  
  873. void UPlayerMovementController::Standard_Pound_Implementation(EStateInput input)
  874. {
  875. if (CommonOnLandedActions(input)) { return; }
  876.  
  877. switch (input)
  878. {
  879. case INIT:
  880. Parent->_SetMoveState(EMoveState::Pounding);
  881. Parent->LaunchCharacter({ 0.f, 0.f, ((DownwardLaunchVelocity <= 0.f) ? DownwardLaunchVelocity : (DownwardLaunchVelocity * -1.f)) }, false, true);
  882. break;
  883.  
  884. case EVENT_Landed:
  885. NewState(EMoveState::Idle);
  886. break;
  887.  
  888. default:
  889. break;
  890. }
  891. }
  892.  
  893.  
  894. void UPlayerMovementController::Standard_PoundFlip_Implementation(EStateInput input)
  895. {
  896. switch (input)
  897. {
  898. case INIT:
  899. Parent->_SetMoveState(EMoveState::PoundFlipping);
  900. Ref_CharacterMovement->Velocity = { 0.f, 0.f, 0.f };
  901. Ref_CharacterMovement->GravityScale = 0.f;
  902. Parent->GetWorldTimerManager().SetTimer(PoundFlipTimer, this, &UPlayerMovementController::PoundFlipTimerHandler, PoundFlipDuration, false);
  903. break;
  904.  
  905. case JUMP_Pressed:
  906. if (DivesAttempted < MaxDives) {
  907. NewState(EMoveState::Diving);
  908. }
  909. break;
  910.  
  911. case TIMER_PoundFlip:
  912. NewState(EMoveState::Pounding);
  913. break;
  914.  
  915. case END:
  916. Parent->GetWorldTimerManager().ClearTimer(PoundFlipTimer);
  917. Ref_CharacterMovement->GravityScale = 1.f;
  918. break;
  919.  
  920. default:
  921. break;
  922. }
  923. }
  924.  
  925.  
  926. void UPlayerMovementController::PoundFlipTimerHandler()
  927. {
  928. (this->*StateFunction)(EStateInput::TIMER_PoundFlip);
  929. }
  930.  
  931.  
  932. void UPlayerMovementController::Standard_Dive_Implementation(EStateInput input)
  933. {
  934. if (CommonOnLandedActions(input)) { return; }
  935. if (__JumpBuffer(input)) { return; }
  936. if (__JumpGravityScaling(input)) { return; }
  937. if (__StickyLanding(input)) { return; }
  938.  
  939. switch (input)
  940. {
  941. case INIT:
  942. Parent->_SetMoveState(EMoveState::Diving);
  943. DivesAttempted++;
  944.  
  945. if (Parent->IsApplyingInput())
  946. {
  947. // If applying input, dive in input direction
  948. FRotator ControlRotation = Parent->GetControlRotation();
  949. FRotator NormalizedControlRotation{ 0.f, ControlRotation.Yaw, 0.f };
  950. FVector ControlRightVector = UKismetMathLibrary::GetRightVector(NormalizedControlRotation);
  951. FVector ControlForwardVector = UKismetMathLibrary::GetForwardVector(NormalizedControlRotation);
  952. float HorizontalAxisInput = GetHorizontalAxisValue();
  953. float VerticalAxisInput = GetVerticalAxisValue();
  954. FVector InputVector = ((ControlRightVector * HorizontalAxisInput) + (ControlForwardVector * VerticalAxisInput)); // Calc input vector
  955. FVector2D Normalized2DInputVector{ InputVector.X, InputVector.Y }; // We want to normalize the 2D components of the input vector
  956. UKismetMathLibrary::Normalize2D(Normalized2DInputVector);
  957. FVector DiveVector{ (Normalized2DInputVector.X * DiveForwardVelocity), (Normalized2DInputVector.Y * DiveForwardVelocity), DiveZVelocity };
  958. Parent->LaunchCharacter(DiveVector, true, true);
  959. }
  960. else
  961. {
  962. // Otherwise, dive in the direction currently facing
  963. FVector CapsuleForwardVector = Parent->GetCapsuleComponent()->GetForwardVector();
  964. FVector DiveVector = CapsuleForwardVector * DiveForwardVelocity;
  965. DiveVector.Z = DiveZVelocity;
  966. Parent->LaunchCharacter(DiveVector, true, true);
  967. }
  968.  
  969. break;
  970.  
  971. case CROUCH_Pressed:
  972. NewState(EMoveState::PoundFlipping);
  973. return;
  974.  
  975. case HIT_Pressed:
  976. if (SpinJumpsAttempted < MaxSpinJumps)
  977. {
  978. Parent->FireHitMechanic();
  979. }
  980. break;
  981.  
  982. case EVENT_HitMechanic:
  983. NewState(EMoveState::SpinJumping);
  984. return;
  985.  
  986. case EVENT_Landed:
  987. NewState(EMoveState::Idle);
  988. return;
  989.  
  990. default:
  991. break;
  992. }
  993. }
  994.  
  995.  
  996. void UPlayerMovementController::Standard_Dodge_Implementation(EStateInput input)
  997. {
  998. if (CommonOnLandedActions(input)) { return; }
  999. if (_JumpTransitionGuard(input)) { return; }
  1000. if (__JumpBuffer(input)) { return; }
  1001. switch (input)
  1002. {
  1003. case INIT:
  1004. {
  1005. Parent->_SetMoveState(EMoveState::Dodging);
  1006. UCapsuleComponent* _CapsuleComponent = Parent->GetCapsuleComponent();
  1007. FVector CapsuleForwardVector = _CapsuleComponent->GetForwardVector();
  1008. CapsuleForwardVector = CapsuleForwardVector * DodgeForwardVelocity;
  1009. FVector _LaunchVector{ CapsuleForwardVector.X, CapsuleForwardVector.Y, DodgeUpVelocity };
  1010. Parent->LaunchCharacter(_LaunchVector, true, false);
  1011. break;
  1012. }
  1013.  
  1014. case TICK:
  1015. if (!Ref_CharacterMovement->IsFalling() && !JumpInputBuffered)
  1016. {
  1017. NewState(EMoveState::Idle);
  1018. return;
  1019. }
  1020. break;
  1021.  
  1022. case EVENT_Landed:
  1023. NewState(EMoveState::Idle);
  1024. return;
  1025.  
  1026. default:
  1027. break;
  1028. }
  1029. }
  1030.  
  1031.  
  1032. bool UPlayerMovementController::CommonActions_Implementation(EStateInput input)
  1033. {
  1034. switch(input)
  1035. {
  1036. case INIT:
  1037.  
  1038. break;
  1039.  
  1040. case HIT_Pressed:
  1041. Parent->FireHitMechanic();
  1042. break;
  1043.  
  1044. case FLASH_Pressed:
  1045. Parent->FireCameraMechanic();
  1046. break;
  1047.  
  1048. default:
  1049. break;
  1050. }
  1051. return false;
  1052. }
  1053.  
  1054.  
  1055. bool UPlayerMovementController::CommonJumpActions_Implementation(EStateInput input)
  1056. {
  1057. if (__JumpBuffer(input)) { return true; }
  1058. if (__JumpGravityScaling(input)) { return true; }
  1059. if (__StickyLanding(input)) { return true; }
  1060.  
  1061. switch (input)
  1062. {
  1063. case TICK:
  1064. AirMovement();
  1065. break;
  1066.  
  1067. case END:
  1068. //Parent->JumpMaxCount = 1;
  1069. //Parent->StopJumping();
  1070. break;
  1071.  
  1072. case HIT_Pressed:
  1073. if (SpinJumpsAttempted < MaxSpinJumps) {
  1074. Parent->FireHitMechanic();
  1075. }
  1076. break;
  1077.  
  1078. case FLASH_Pressed:
  1079. Parent->FireCameraMechanic();
  1080. break;
  1081.  
  1082. case CROUCH_Pressed:
  1083. NewState(EMoveState::PoundFlipping);
  1084. return true;
  1085.  
  1086. case EVENT_HitMechanic:
  1087. NewState(EMoveState::SpinJumping);
  1088. return true;
  1089.  
  1090. default:
  1091. break;
  1092. }
  1093. return false;
  1094. }
  1095.  
  1096.  
  1097. bool UPlayerMovementController::__StickyLanding(EStateInput input)
  1098. {
  1099. switch (input)
  1100. {
  1101. case EVENT_Landed:
  1102. if (!Parent->IsApplyingInput())
  1103. {
  1104. FVector CharacterMovementVelocity = Ref_CharacterMovement->Velocity;
  1105. Ref_CharacterMovement->Velocity = FVector(0.f, 0.f, CharacterMovementVelocity.Z);
  1106. }
  1107. break;
  1108.  
  1109. default:
  1110. break;
  1111. }
  1112. return false;
  1113. }
  1114.  
  1115.  
  1116. bool UPlayerMovementController::__JumpBuffer(EStateInput input)
  1117. {
  1118. switch (input)
  1119. {
  1120. case INIT:
  1121.  
  1122. break;
  1123.  
  1124. case TICK:
  1125. if (_JumpBuffered)
  1126. {
  1127. _JumpBufferTime += _Internal_DeltaTime;
  1128. }
  1129. break;
  1130.  
  1131. case JUMP_Pressed:
  1132. if (_JumpBuffered)
  1133. {
  1134. _JumpBufferTime = 0.f;
  1135. }
  1136. else
  1137. {
  1138. _JumpBuffered = true;
  1139. }
  1140. break;
  1141.  
  1142. case EVENT_Landed:
  1143. if (_JumpBuffered && (_JumpBufferTime < JumpBufferMaxTime))
  1144. {
  1145. NewState(EMoveState::Jumping);
  1146. return true;
  1147. }
  1148. break;
  1149.  
  1150. case END:
  1151. _JumpBuffered = false;
  1152. _JumpBufferTime = 0.f;
  1153. Parent->StopJumping();
  1154. break;
  1155.  
  1156. default:
  1157. break;
  1158. }
  1159. return false;
  1160. }
  1161.  
  1162.  
  1163. bool UPlayerMovementController::__JumpGravityScaling(EStateInput input)
  1164. {
  1165. switch (input)
  1166. {
  1167. case INIT:
  1168.  
  1169. break;
  1170.  
  1171. case TICK:
  1172. {
  1173. FVector CapsuleVelocity = Parent->GetCapsuleComponent()->GetComponentVelocity();
  1174. if (CapsuleVelocity.Z > JumpVelocityUpThreshold)
  1175. {
  1176. Ref_CharacterMovement->GravityScale = GravityScaleUp;
  1177. }
  1178. else if (CapsuleVelocity.Z < JumpVelocityDownTreshold)
  1179. {
  1180. Ref_CharacterMovement->GravityScale = GravityScaleDown;
  1181. }
  1182. else
  1183. {
  1184. Ref_CharacterMovement->GravityScale = GravityScalePeak;
  1185. }
  1186. break;
  1187. }
  1188. case END:
  1189. Ref_CharacterMovement->GravityScale = 1.f;
  1190. break;
  1191.  
  1192. default:
  1193. break;
  1194. }
  1195. return false;
  1196. }
  1197.  
  1198.  
  1199. bool UPlayerMovementController::_JumpTransitionGuard(EStateInput input)
  1200. {
  1201. switch(input)
  1202. {
  1203. case INIT:
  1204. JumpInputBuffered = true;
  1205. break;
  1206.  
  1207. case EVENT_Jumped:
  1208. //JumpInputBuffered = false;
  1209. break;
  1210.  
  1211. case EVENT_Launched:
  1212. JumpInputBuffered = false;
  1213. break;
  1214.  
  1215. case EVENT_Landed:
  1216. JumpInputBuffered = false;
  1217. break;
  1218.  
  1219. case END:
  1220. JumpInputBuffered = false;
  1221. break;
  1222.  
  1223. default:
  1224. break;
  1225. }
  1226. return false;
  1227. }
  1228.  
  1229.  
  1230. bool UPlayerMovementController::NormalMovement()
  1231. {
  1232. if (Parent->IsApplyingInput()) // todo: does this applying input check have to happen?
  1233. {
  1234. FRotator PlayerForwardRotation = UKismetMathLibrary::MakeRotFromX(Parent->GetCapsuleComponent()->GetForwardVector());
  1235.  
  1236. /* Rotation between input vector and character forward vector. */
  1237. float AbsRotation = _GetAbsoluteRotationBetweenYaws(PlayerForwardRotation, InputRotationFromX);
  1238.  
  1239. if (Parent->GetVelocityVectorLength() < 300.f) // if moving slowly
  1240. {
  1241. _RotateInstantToInputDirection(); // just rotate to the input direction
  1242. _AddForwardMovement();
  1243. }
  1244. else if (AbsRotation > 170.f) // otherwise, if applying input directly opposite the vector of movement, rotate instantly and skid turn
  1245. {
  1246. // todo: skid turn
  1247. }
  1248. else // otherwise, rotate slowly and move as normal
  1249. {
  1250. _RotateConstantToInputDirection(RotationSpeedSlow);
  1251. _AddForwardMovement();
  1252. }
  1253. return true;
  1254. }
  1255. else {
  1256. return false;
  1257. }
  1258. }
  1259.  
  1260.  
  1261. bool UPlayerMovementController::AirMovement()
  1262. {
  1263. if (Parent->IsApplyingInput())
  1264. {
  1265. _AddRelativeMovement();
  1266. _RotateConstantToInputDirection(RotationSpeedSlow);
  1267. //todo: does extra velocity need to be added?
  1268. return true;
  1269. }
  1270. else
  1271. {
  1272. return false;
  1273. }
  1274. }
  1275.  
  1276.  
  1277. void UPlayerMovementController::_AddForwardMovement()
  1278. {
  1279. FVector PlayerForwardVector = Parent->GetCapsuleComponent()->GetForwardVector();
  1280. FVector RawInputUnitVector;
  1281. float RawInputVectorLength;
  1282. Parent->GetRawInputVector().ToDirectionAndLength(OUT RawInputUnitVector, OUT RawInputVectorLength);
  1283. Parent->AddMovementInput(PlayerForwardVector, RawInputVectorLength, false);
  1284. }
  1285.  
  1286.  
  1287. void UPlayerMovementController::_AddRelativeMovement()
  1288. {
  1289. FRotator PlayerControlRotation = Parent->GetControlRotation();
  1290. FRotator NormalizedControlRotation{ 0.0, PlayerControlRotation.Yaw , 0.0f };
  1291. float HorizontalMovement = GetHorizontalAxisValue();
  1292. float VerticalMovement = GetVerticalAxisValue();
  1293. Parent->AddMovementInput(UKismetMathLibrary::GetRightVector(NormalizedControlRotation), HorizontalMovement, false)