Part 11 - Vertical Movement
April 11, 2019

We have spent the last two parts working on the PlayerPiece and we are still not done with it. The thing is, we don't have the vertical movement yet! We will have to incorporate some extra code that will be necessary to correctly implement this movement.

The Floor

As explained in the last part, our animation system requires previous knowledge of the destination vertical coordinate. I did use the term floor quite a few times throughout the tutorial to reference the first empty cell where the player piece can land in an specified column. In the AGameModeInGame class we have internal data, mGridData array, where we keep track of what is inside the grid, including entries to empty cells. This array holds objects of the FGridCelData struct and one could easily think about adding a new floor property into this struct however we don't need this for every single cell. We only need a single integer for each column. For that, we create an new array property, TArray<int32>, and each entry in it will reference a column of the same index.

This approach saves a bit of unnecessary storage at the cost of having an extra upkeep step, which is keeping both the mGridData and the new array in sync. That said, in the private section of the AGameModeInGame class:

GameModeInGame.h
TArray<int32> mColumnFloor;

We have to initialize this vector, creating mGridColumnCount entries and having each one as 0. We initialized mGridData in the OnConstruction() function. Let's add the mColumnFloor initialization in there too. Right after the loop where we setup the mGridData array:

GameModeInGame.cpp
void AGameModeInGame::OnConstruction(const FTransform& Transform)
{
   ...
   // Initialize the mColumnFloor array
   mColumnFloor.SetNum(mGridColumnCount);

   // Probably not necessary, but making sure of this value should avoid some unpleasant bugs
   for (int32 i = 0; i < mGridColumnCount; i++)
   {
      mColumnFloor[i] = 0;
   }
   ...
}

Let's study a little about how this floor value will help us. When the grid is completely empty, we know for sure that at each column our first empty row that can receive blocks is 0. Thus, setting each entry in the mColumnFloor to 0 is correct. Now, suppose that at column 0 a piece, formed by 3 blocks, has landed. This means that rows 0, 1 and 2 are now occupied and mColumnFloor[0] must now be set to 3. With this we have direct access to the cell location corresponding to where the player piece has to end its movement.

Some derived classes may need access to the floor level, however we did add the array in the private section, meaning that we can't directly use mColumnFloor. Let's add a public function meant to return the desired value:

GameModeInGame.h
UFUNCTION(BlueprintPure)
int32 GetFloor(int32 Column) const { return mColumnFloor[Column]; }

The next step is update the floor level, an entry in mColumnFloor, whenever a block is added into the grid. The easiest way here is to perform this update inside the AddBlockToGridData() function. In here, we would take the column to access the correct entry in the mColumnFloor array and then set its value to the row above the cell where the block has been added. In order for this to work correctly we have to make sure the blocks are always added in a bottom-up direction, otherwise the "floor" level can potentially point to cells occupied by other blocks. The safe solution here is to sacrifice this easy/efficient updating by creating a function that scans the grid, from top to bottom, and once a block is found, the scanning ends for that column. Since the grid contains an small amount of cells, it's really unlikely that this function will cause a negative impact, specially if never used at every single frame update. With that said, let's create a public function in the AGameModeInGame class meant to perform this scanning:

GameModeInGame.h
UFUNCTION(BlueprintCallable)
void CheckGridFloorLevels();

In its implementation we have to scan each column and stop as soon as a block is found. Remember that to go a row bellow we have to subtract mGridColumnCount from the index value currently being used. To simplify things, at the beginning of the function a variable, ref_index, is set as an index reference, which points to the top left cell of the grid. This variable is probably the most important aspect to be understood since the index used to read the mGridData array is computed from it. The external loop, which goes through each column has the column index. Just adding this index to ref_index "moves" to the relevant column. From there, each column can be read by subtracting mGridColumnCount. This reading index is controlled by the read_index variable. Then there is the read_row control variable, which is used to keep track of the actual row index, so it can be used to set the "floor" inside the mColumnFloor array. Hopefully this explanation is enough for understanding the code:

GameModeInGame.cpp
void AGameModeInGame::CheckGridFloorLevels()
{
   const int32 ref_index = (mGridRowCount - 1) * mGridColumnCount;

   for (int32 col = 0; col < mGridColumnCount; col++)
   {
      // Assume the "floor" is above the grid
      mColumnFloor[col] = -1;

      int32 read_index = ref_index + col;
      int32 read_row = mGridRowCount - 1;
      while (read_index >= 0)
      {
         if (mGridData[read_index].BlockActor)
         {
            read_index = -1;    // this will stop the internal loop
         }
         else
         {
            // Reading cell is empty - assume this is the floor of this column
            mColumnFloor[col] = read_row;

            read_row--;
            read_index -= mGridColumnCount;
         }
      }
   }
}

Remember the function AGMInGameTraditional::CustomGameInit? We have to update it so the floor levels are set! It's incredibly simple, really! All that has to be done is add CheckGridFloorLevels(); right after the loop that spawns the initial blocks:

GMInGameTraditional.cpp
void AGMInGameTraditional::CustomGameInit()
{
   ...
   CheckGridFloorLevels();

   Super::CustomGameInit();
}

Falling Piece

Now we have the necessary internal data to implement the vertical movement of the player piece. Let's begin by adding the function to setup the movement itself, in the FPlayerPiece "class", in the public section:

PlayerPiece.h
void VerticalMove(float VerticalPosition, float TimeLimit);

The logic in this function is very similar to the one used in the SideMove() function. However, each block in the piece will have to land in a different cell. Only the first block will end in the specified coordinate. We have to calculate the correct places. We could have done that through an extra parameter specifying the offset for each block. However, we can easily obtain this offset from the blocks inside the player piece.

PlayerPiece.cpp
void FPlayerPiece::VerticalMove(float VerticalPosition, float TimeLimit)
{
   // Calculate the offset coordinate between each block
   const float coord_offset = mBlock[1]->GetActorLocation().Z - mBlock[0]->GetActorLocation().Z;

   mVerticalTime.Set(TimeLimit);

   float coord = VerticalPosition;
   for (ABlock* block : mBlock)
   {
      block->InitOriginalPosition();
      block->SetupVertical(coord);

      coord += coord_offset;        // offset upwards
   }
}

In order to animate the vertical movement we have to update the Tick() function to properly update the coordinate. Remember that we have a temporary line in this function? More specifically, this:

PlayerPiece.cpp
// Temporarily get the Z coordinate - this will be computed correctly later
interp_position.Z = block->GetActorLocation().Z;

It should be substituted by this:

PlayerPiece.cpp
// Interpolate the vertical position
block->InterpolateVertical(valpha, interp_position.Z);

Wait, we are not checking the return value of the function! Indeed, not yet. The thing is, we didn't add any code to deal with the event of the player piece actually reaching its destination and it requires some more changes. First let's make the vertical movement work then we come back and fix this fact.

Next, we have to setup the vertical movement as soon as the player piece is spawned. For that, we can update the StateSpawning() function (in the AGameModeInGame class) and add the vertical setup code right after we call the SetCurrentColumn() function. The destination coordinate is very easy. With the floor information, we can just use that to request the cell location:

GameModeInGame.cpp
AGameModeInGame::StateFunctionProxy AGameModeInGame::StateSpawning(float Seconds)
{
   ...
   else
   {
      ...
      // Set the correct column
      mPlayerPiece.SetCurrentColumn(spawn_col);

      // Setup the vertical movement
      const float vert_dest = GetCellLocation(spawn_col, mColumnFloor[spawn_col]).Z;
      const float vert_time = mPlayerPiece.GetVertDiff(vert_dest) / mPlayField->GetScaledCellSize() * UColBPLibrary::GetVerticalMoveTime(this);

      mPlayerPiece.VerticalMove(vert_dest, vert_time);
      ...
   }
   ...
}

Combined Movement Fix

If you build the project and test it in this state you will see the piece is now falling. However, if you move to another column the blocks will warp bellow and the falling speed will decrease. This happens because we are resetting the original reference position used for animation but are not updating the time limit. There is another detail that we have to take into account when fixing this, which is also related to the time limit. When moving to a different column the floor level is potentially different, meaning that we have to re-setup the vertical movement. Also remember that we are not dealing with the event when te piece reaches its destination. The behavior at current state is that we can still control the piece when it lands. We will deal with all of that in this part.

As said, to fix the horizontal movement we have to update the time limit as well as the destination vertical coordinate. As it turns out it's pretty easy to do that. When we deal with the side movement event, besides setting up the horizontal movement, we also have to setup the vertical movement, in the very same way we did in the player piece spawning. With that said, let's update the AGameModeInGame::OnSideMove() function. It should look like this:

GameModeInGame.cpp
void AGameModeInGame::OnSideMove(float AxisValue)
{
   ...
   else
   {
      // And we can also leave if the "timer does not allow"
      if (mSideMoveTimer > 0.0f)
         return;

      // AxisValue == 0 has already been dealt with, so this assignment is valid
      const int32 dest_col = mPlayerPiece.GetCurrentColumn() + (AxisValue < 0.0f ? -1 : 1);

      // Check grid borders
      if (dest_col < 0 || dest_col >= GetColumnCount())
         return;

      // Obtain the destination coordinates
      const FVector dest_coord = GetCellLocation(dest_col, mColumnFloor[dest_col]);
      
      // Calculate the time limit fraction
      const float htime_fraction = mPlayerPiece.GetHorizDiff(dest_coord.X) / mPlayField->GetScaledCellSize() * UColBPLibrary::GetHorizontalMoveTime(this);
      const float vtime_fraction = mPlayerPiece.GetVertDiff(dest_coord.Z) / mPlayField->GetScaledCellSize() * UColBPLibrary::GetVerticalMoveTime(this);

      // Move the piece
      mPlayerPiece.SideMove(dest_coord.X, htime_fraction);
      mPlayerPiece.VerticalMove(dest_coord.Z, vtime_fraction);

      // And update the player piece column
      mPlayerPiece.SetCurrentColumn(dest_col);

      // Setup the input timing so we don't get uncontrollable movement
      mSideMoveTimer = UColBPLibrary::GetSideMoveDelay(this);
   }
}

The changes are very subtle. There are a few things to notice. First, the dest_coord, which was previously a float is now a vector. Then, we are calling both SideMove() and VerticalMove() whenever performing the horizontal movement. If you go back to the implementations of those functions, both are calling InitOriginalPosition(). There is no need to to that. Since we have to call the VerticalMove() after setting the horizontal movement, we can remove the InitOriginalPosition() line from the SideMove() function. It now looks like this:

PlayerPiece.cpp
void FPlayerPiece::SideMove(float HorizontalPosition, float TimeLimit)
{
   mHorizontalTime.Set(TimeLimit);
   mIsSideMoving = true;
   for (ABlock* block : mBlock)
   {
      block->SetupHorizontal(HorizontalPosition);
   }
}

Moving the piece horizontally now should not interfere with the vertical movement.

Landing the Piece

At this point, the player piece is falling and correctly reacting to player input. However, when it lands "nothing" happens. The player piece remains controllable but since it reached its vertical destination, there is no more "falling" for it. The first thing that should happen when the player piece lands is add all of its blocks into the grid, however the FPlayerPiece doesn't, and shouldn't, have access to the grid. Still, it's the only piece of code that knows when the blocks have landed. We can setup a flag in the player piece and after each time the Tick() function is called we check if it has landed which provides us the condition to transition into the Check Match state. With this in mind, let's add a new private property into the FPlayerPiece struct:

PlayerPiece.h
bool mHasLanded;

In the constructor we initialize it:

PlayerPiece.h
FPlayerPiece()
   : mIsSideMoving(false)
   , mHasLanded(false)
{}

Remember that a player piece should always be falling whenever it's spawned. So, in the spawn function we add the line to set this flag to false:

PlayerPiece.h
template <typename GetBlockFunc>
void SpawnPiece(GetBlockFunc Func)
{
   mHasLanded = false;
   ...
}

Of course, we need means to obtain this. A public function:

PlayerPiece.h
bool HasLanded() const { return mHasLanded; }

In the Tick() function we have to update this flag if the piece has landed. We can take advantage of the value returned by InterpolateVertical(). Since it returns true when the piece has reached its destination, we can directly set the return value to the flag. This means, the line block->InterpolateVertical(valpha, interp_position.Z); should be changed into:

PlayerPiece.cpp
// Interpolate the vertical position
mHasLanded = block->InterpolateVertical(valpha, interp_position.Z);

We can now check if the player has landed, right after mPlayerPiece.Tick() and once it does, we have to take the blocks forming the player piece and "transfer" them into the grid. However we don't have access to each individual block from outside of the player piece. We can solve this by creating a function that will call a function object for each of the internal blocks, passing the block pointer as parameter to this object. I will explain how we will use this right after the code of this public function that has to be added into the FPlayerPiece struct:

PlayerPiece.h
template <typename RetBlockFunc>
void ForEachBlock(RetBlockFunc Func)
{
   for (class ABlock* b : mBlock)
   {
      Func(b);
   }
}

Oce we get true from mPlayerPiece.HasLanded() we can call mPlayerPiece.ForEachBlock() with a lambda that will add the received parameter into the grid and at the same time update the column's floor. We already have means to obtain every necessary information. With mPlayerPiece.GetCurrentColumn() we obtain the column where the piece has landed then, with that, the floor level. As already said, the floor value indicates the first empty row or, in other words, where the first (bottom most) block of the player piece has to be added. Then, near the end of the lambda function we increment the floor value so at the next iteration of the ForEachBlock() we add the next block into the row above. When the last iteration occurs the floor value will be correctly "pointing" to the first empty row. The last task is to clear the internal pointers of the player piece. This is probably not really necessary but since we don't need to hold them there, we will do so.

Before (re)working the Playtime state to make use of the new code, let's add the cleanup code in the public section of the FPlayerPiece:

PlayerPiece.h
void Clear();

In its implementation all we do is iterate through each entry in the mBlock array setting it to nullptr:

PlayerPiece.cpp
void FPlayerPiece::Clear()
{
   const int32 count = mBlock.Num();
   for (int32 i = 0; i < count; i++)
   {
      mBlock[i] = nullptr;
   }
}

One last thing before looking into the StatePlaytime() code. If you take a look at the state machine design, part 7, the only condition needed to transition from the Playtime state into the Check Match state is the player piece landing. This means that once we transfer the blocks from the player piece into the grid and update the floor level, we have to return StateCheckMatch() from the Playtime state function. Finally, let's see the new code in the AGameModeInGame::StatePlaytime():

GameModeInGame.cpp
AGameModeInGame::StateFunctionProxy AGameModeInGame::StatePlaytime(float Seconds)
{
   mPlayerPiece.Tick(Seconds);

   if (mPlayerPiece.HasLanded())
   {
      // Obtain the column where the piece has landed
      const int32 column = mPlayerPiece.GetCurrentColumn();

      mPlayerPiece.ForEachBlock([this, &column](ABlock* block)
      {
         // Add the block into the grid data
         AddBlockToGridData(GetCellIndex(column, mColumnFloor[column]), block);

         // And update the floor level
         mColumnFloor[column]++;
      });

      // Cleanup the player piece's internal data
      mPlayerPiece.Clear();

      // Transition into the `Check Match` state
      return &AGameModeInGame::StateCheckMatch;
   }

   return &AGameModeInGame::StatePlaytime;
}

If you build and test at this moment you will notice that once the player piece reaches the destination the game will look like frozen. The thing is, we have transitioned into the StateCheckMatch() state function which is "empty". We could just "short circuit" and instead of going into the Check Match state, set to transition into the Spawning state just for the sake of testing new pieces being spawned and if our floor value updating is correct. I will not go into the steps for doing that since it's incredible simple and at this moment you should be able to do that on your own! If you do so, know that there are a few problems that we still have to deal with (we will deal with them soon):

  1. We have not added any code to check if the game has been lost. With that, if there is not enough rows to spawn the blocks of the player piece, you will get a crash since the spawning algorithm will try to access an invalid grid data index.
  2. There is still no horizontal "collision" code between the player piece and blocks that are already in the grid. So, if you have a "tower" of blocks and you try to move the player piece into it, instead of blocking the movement the piece will actually get inside the blocks and will start moving up. Indeed, the floor level of that column is above the bottom of the player piece!

Now, what if we want a game mode to do something specific whenever the player piece lands? With our current code we can't do that, but we can remedy this with an event, with possibility to implement through blueprints. Following the same kind of pattern we have done in early parts, we will add a native virtual function that will, by default, call in the blueprint version. With that said, let's add the two functions into the public section of the AGameModeInGame:

GameModeInGame.h
// Native C++ event code called whenever a player piece finishes it's vertical movement
virtual void OnPlayerPieceLanded(const TArray<int32>& BlockIndices) { BP_OnPlayerPieceLanded(BlockIndices); }

// Blueprint event called whenever a player piece finishes it's vertical movement
UFUNCTION(BlueprintImplementableEvent, meta = (DisplayName = "OnPlayerPieceLanded"))
void BP_OnPlayerPieceLanded(const TArray<int32>& BlockIndices);

Notice the argument, an Array of integer values. Each entry in this array is meant to be the index in the grid data pointing to the place where the new blocks have been added. With those indices we can retrieve the block pointer (a new function for that will be added soon). But, why not directly give access to the block pointers instead? We will make use of the data stored in the array for the match checking really soon and instead of creating/filling two arrays (one for the block pointers and the other for the indices), we just make one that can be used to access all the data we need. With that said, let's declare a private array, in the AGameModeInGame class, that we will fill in and send to the OnPlayerPieceLanded() function:

GameModeInGame.h
TArray<int32> mLandedBlock;

We go back into the StatePlaytime() state function so we can fill this new array and fire up the event OnPlayerPieceLanded():

GameModeInGame.cpp
AGameModeInGame::StateFunctionProxy AGameModeInGame::StatePlaytime(float Seconds)
{
   ...
   if (mPlayerPiece.HasLanded())
   {
      // Obtain the column where the piece has landed
      const int32 column = mPlayerPiece.GetCurrentColumn();

      // Make sure the landed blocks array is empty
      mLandedBlock.Empty();

      mPlayerPiece.ForEachBlock([this, &column](ABlock* block)
      {
         // Get the cell index where the block is being added
         const int32 cell_index = GetCellIndex(column, mColumnFloor[column]);

         // Add the index into the landed array
         mLandedBlock.Add(cell_index);

         // Add the block into the grid data
         AddBlockToGridData(cell_index, block);

         // And update the floor level
         mColumnFloor[column]++;
      });

      // Cleanup the player piece's internal data
      mPlayerPiece.Clear();

      // Fire up the event
      OnPlayerPieceLanded(mLandedBlock);

      // Transition into the `Check Match` state
      return &AGameModeInGame::StateCheckMatch;
   }
   ...
}

Horizontal Collision

A little early in this part I have mentioned two problems with the current state of the code. First we will tackle the lack of "side collisions", that is, disallow moving the player piece if the result of the movement would collide with previously landed blocks. The algorithm to perform this check is pretty simple, really. We need to obtain the vertical coordinate of the bottom most block of the player piece (let's call it PzP_z) and compare with the vertical coordinate of the floor cell of the destination column (let's call it FzF_z). If Pz<FzP_z < F_z then it means the player piece is bellow floor level of the destination column and the horizontal movement can't be performed in this case.

In order to obtain the PzP_z we will need to add a new (public) function into the FPlayerPiece, meant to return just the vertical coordinate of the bottom most block in its internal array:

PlayerPiece.h
int32 GetPieceZ() const;

The implementation;

PlayerPiece.cpp
int32 FPlayerPiece::GetPieceZ() const
{
   return mBlock[0]->GetActorLocation().Z;
}

We now can update our AGameModeInGame::OnSideMove() function to perform this collision check:

GameModeInGame.cpp
void AGameModeInGame::OnSideMove(float AxisValue)
{
   ...
   else
   {
      ...
      // Obtain the destination coordinates
      const FVector dest_coord = GetCellLocation(dest_col, mColumnFloor[dest_col]);

      // Check horizontal collision
      if (mPlayerPiece.GetPieceZ() < dest_coord.Z)
         return;
      
      ...
   }
}

That's it! We can no longer move a piece on top of blocks that have previously landed.

Horizontal Movement Landing Fix

With current code, if the player piece "lands" while it's in the middle of the horizontal movement, it will be placed in the same horizontal coordinate of the animation at the landing time. Internally it's still occupying the destination column but visually it's in the middle of the movement. To fix that, at the time we call the interpolation function to perform the horizontal movement we pass in an alpha equal to 1.0 if the piece has landed. The relevant snippet from the FPlayerPiece::Tick() function goes here:

PlayerPiece.cpp
void FPlayerPiece::Tick(float DeltaSeconds)
{
   ...
   for (ABlock* block : mBlock)
   {
      ...
      // Perform side horizontal movement if necessary
      if (mIsSideMoving)
      {
         if (block->InterpolateHorizontal(mHasLanded ? 1.0f : halpha, interp_position.X))
         {
            mIsSideMoving = false;
         }
      }      
      ...
   }
}

This code fixes the piece being placed in a visually wrong location when it lands in the middle of horizontal movement, however on certain situations you will probably notice the piece being "sucked" into the destination position, specially if it lands at the very beginning of the horizontal movement. This behavior can't be fully eliminated because we have discreet final positions while we are allowing visually continuos positioning. We can mitigate this if instead of setting the blocks to occupy the destination column we instead place them in the closest column to their visual representation. That case, however, requires a lot of extra code in order to work. And, no, the interpolation factor, halpha in this case, cannot be used to obtain the closest column because we are allowing the player to change movement direction in the middle of a previous animation.

Loosing Condition

Right now, we can test how the project is progressing, however it will crash if there is not enough rows to spawn a new player piece in the center column of the grid. This is because we are not performing the loosing condition check. Let's fix that! Currently, the testing level is using a blueprint game mode derived from the AGMInGameDefault game mode class which, in turn, is derived from AGameModeInGame. Remember that we did create a virtual function (IsGameLost()) in the AGameModeInGame meant to give some flexibility when dealing with the game lost condition? We can either implement this function in the Blueprint version of the game mode or in the derived native AGMInGameTraditional class. For this case we will use the native C++ code and in a subsequent game mode work with blueprint. Anyway, it's necessary to declare the override in the public section:

GMInGameTraditional.h
virtual bool IsGameLost() override;

To implement this all we have to do is check if the column where the piece will be spawned has enough rows to fit all blocks of the new piece. The data we need is the column where the blocks should be placed, the floor of that column, the total number of rows and the amount of blocks forming the player piece. We already have easy access to all of those. Then we have to obtain the difference (subtraction) between the floor and the total number of rows (let's call it RR - amount of rows left) and compare the result with the player piece size (let's call it PsP_s). If R<PsR < P_s then it means we don't have enough rows to place all of the new blocks of the player piece, meaning, game lost! The code:

GMInGameTraditional.cpp
bool AGMInGameTraditional::IsGameLost()
{
   // The column where the piece should be spawned
   const int32 spawn_col = GetColumnCount() / 2;

   // Amount of rows left
   const int32 rows_left = GetRowCount() - GetFloor(spawn_col);

   // Game is lost is rows left is smaller than the player piece size
   return (rows_left < UColBPLibrary::GetPlayerPieceSize(this));
}

At this moment, when this condition happens, the state machine will transition into the Game Lost state but we will not know anything since we didn't add any visual feedback. Just for the sake of testing, let's add some temporary code into the StateGameLost() state function. All we will do is print a red colored debug message that will last 10 seconds on screen, through the usage of engine's function AddOnScreenDebugMessage(), and then transition into the End Game state:

GameModeInGame.cpp
AGameModeInGame::StateFunctionProxy AGameModeInGame::StateGameLost(float Seconds)
{
   GEngine->AddOnScreenDebugMessage(-1, 10.0f, FColor::Red, "Game Lost");

   return &AGameModeInGame::StateEndGame;
}

In order to test this new code it's necessary to have "short circuited" the state transition after the player piece landed, as discussed early in this part. Again, I will not show the exact code in here.

The "Accelerate" Button

We did create input mapping to increase the speed in which the player piece falls, however there is no code (yet) to actually do that. How exactly will we increase the dropping speed of the player piece? We are updating the location of the blocks through the usage of linear interpolation, using time as the alpha/percent. We can update the elapsed time with some multiplier so the value that we pass in to the linear interpolation function increases faster (or slower if desired). So, let's update the FPlayerPiece struct and add a private property that will be used as this multiplier:

PlayerPiece.h
float mVertAlphaMultiplier;

Then we create a public function that allows us to directly set this value:

PlayerPiece.h
void SetVerticalAlphaMultiplier(float Multiplier) { mVertAlphaMultiplier = Multiplier; }

We have to initialize this value in the constructor. It should be 1.0 in order to keep the exact same speed that we are dealing with at the moment:

PlayerPiece.h
FPlayerPiece()
      : mIsSideMoving(false)
      , mHasLanded(false)
      , mVertAlphaMultiplier(1.0f)
   {}

Finally, we have to change just the line where we perform the timing update, inside the Tick() function:

PlayerPiece.cpp
void FPlayerPiece::Tick(float DeltaSeconds)
{
   // Update the timing objects
   ...
   const float valpha = mVerticalTime.Update(DeltaSeconds * mVertAlphaMultiplier);
   ...
}

Next all we have to do is increase the value of the vertical alpha multiplier. Do not set this to a negative value. Doing so will cause the movement to go in the reverse direction and we didn't add any kind of boundary checks for that case. In other words, the game will crash! A positive value, but still smaller than 1.0 will decrease the dropping speed of the player piece.

We have to use this, right? In the game mode (AGameModeInGame) we added two input event functions to deal with the "Accelerate" mapping, OnAccelerate() and OnDecelerate(). Let's add some code to both functions. In the OnAccelerate() we want to increase the dropping speed of the player piece, by setting the vertical alpha multiplier to something bigger than 1. In the OnDecelerate() we want to return to the normal, by resetting the multiplier to 1. Now, which value should we set the multiplier to when increasing the drop speed? That surely calls for a value that we can tweak!

Although in this tutorial I will not add any option for the player to choose how fast the piece should drop when the "accelerate" button is pressed, the property for this multiplier will be added in the game instance class, so should you want to give this option in your project, you will be able to do so. The overall pattern for this was already done numerous times throughout this tutorial, so I will rush quite a bit with the setup of this property. In the private section of the UColGameInstance:

ColGameInstance.h
UPROPERTY(EditAnywhere, meta = (DisplayName = "Vertical Fast Multiplier"))
float mVerticalFastMultiplier;

The public function to get this value:

ColGameInstance.h
UFUNCTION(BlueprintPure)
float GetVerticalFastMultiplier() const { return mVerticalFastMultiplier; }

Initialization:

ColGameInstance.cpp
UColGameInstance::UColGameInstance()
{
   ...
   mVerticalFastMultiplier = 5.0f;
}

The shortcut in the blueprint function library:

ColBPLibrary.h
// Obtain the multiplier used to increase the vertical dropping speed
UFUNCTION(BlueprintPure, meta = (WorldContext = "WorldContextObject"))
static float GetVerticalFastMultiplier(const UObject* WorldContextObject);

And its implementation:

ColBPLibrary.cpp
float UColBPLibrary::GetVerticalFastMultiplier(const UObject* WorldContextObject)
{
   if (UColGameInstance* gi = GetColGameInstance(WorldContextObject))
   {
      return gi->GetVerticalFastMultiplier();
   }
   return 5.0f;
}

Finally, update the mentioned input event functions:

GameModeInGame.cpp
void AGameModeInGame::OnAccelerate()
{
   mPlayerPiece.SetVerticalAlphaMultiplier(UColBPLibrary::GetVerticalFastMultiplier(this));
}

void AGameModeInGame::OnDecelerate()
{
   mPlayerPiece.SetVerticalAlphaMultiplier(1.0f);
}

In this part we have incorporated the vertical movement aspect of the player piece, correctly "landed" the blocks, means to react to the event of the player piece finishing its movement and the last input handling that we needed. If you "short-circuited" the state machine transitions in order to test current code, you will notice that we still need to incorporate the match checking. That's what we will be done in the next part!

Introduction
Previous12345678910
11
121314151617181920212223Next