-->
Now that we have the player piece being spawned in the grid and with the block order rotation caused by player input coded in, we want to incorporate means to move it horizontally, also by player input and later follow our game rules with vertical movement. As defined in the project, both movements must occur smoothly rather than through teleportation. And by smoothly I mean, continuos movement.
As usual, let's see a little bit of theory before implementing anything. If you think about the horizontal movement, the player piece has to move into predetermined coordinates, to match the various columns of the grid. What this means is that whenever the SideMove
mapped button is pressed we will already know the horizontal position where the piece has to be at the end of the movement.
Based on that we can define a property meant to tell how much time is needed to move the piece from one column to another. Once the movement begins, we calculate the amount of time elapsed, compare to the needed time (obtaining a "percent" value) and then use that to interpolate the visual position in which the piece has to be drawn.
An alternative way would be to define a property to indicate the movement speed and, with the elapsed time, just move the piece to the side, much like . Feel free to implement in this way if you prefer.
The vertical movement of the piece will follow very similar idea, although we will need to take into account some details, which will be explained once we get to that part.
We will begin by performing some "blind" horizontal movements and by "teleporting" the piece. By "blind" I mean that we won't check anything besides the confines of the grid. And by "teleporting" I mean directly setting the destination position. Once we get that working we will incorporate the animation.
The very first thing we have to do is add the timing variables meant to avoid horizontally moving the piece more than intended (this was already explained in the previous part, when dealing with the rotation of the blocks). With that said, we add the private property in the UColGameInstance
class:
UPROPERTY(EditAnywhere, meta = (DisplayName = "Side Move Delay"))
float mSideMoveDelay;
In the public section the function to retrieve this value:
UFUNCTION(BlueprintPure)
float GetSideMoveDelay() const { return mSideMoveDelay; }
And, of course, its initialization in the constructor. Again, chances are high that we will need to tweak this from the editor:
UColGameInstance::UColGameInstance()
{
... // Previous code
mSideMoveDelay = 0.35f;
}
And then, we add the "shortcut" function in the blueprint function library:
// Obtain the side move input delay
UFUNCTION(BlueprintPure, meta = (WorldContext = "WorldContextObject"))
static float GetSideMoveDelay(const UObject* WorldContextObject);
Which is implemented like this:
float UColBPLibrary::GetSideMoveDelay(const UObject* WorldContextObject)
{
if (UColGameInstance* gi = GetColGameInstance(WorldContextObject))
{
return gi->GetSideMoveDelay();
}
return 0.35f;
}
Finally, the private property that will be used to actually count the time, inside the AGameModeInGame
:
float mSideMoveTimer;
Don't forget its initialization in the constructor:
AGameModeInGame::AGameModeInGame()
{
...
mShiftTimer = 0.0f;
mSideMoveTimer = 0.0f;
}
Its update in the Tick()
function:
void AGameModeInGame::Tick(float DeltaTime)
{
...
// Update input timers
if (mShiftTimer > 0.0f)
{
mShiftTimer -= DeltaTime;
}
if (mSideMoveTimer > 0.0f)
{
mSideMoveTimer -= DeltaTime;
}
...
}
And its reset after spawning the piece in the StateSpawning()
:
AGameModeInGame::StateFunctionProxy AGameModeInGame::StateSpawning(float Seconds)
{
...
// Reset input timers
mShiftTimer = 0.0f;
mSideMoveTimer = 0.0f;
}
...
}
Sorry if this seemed a bit rushed, with almost no explanation, however I have already covered an almost identical code in the previous part, where I did explain in more details what is going on.
Moving on, in order to keep the piece "confined" within the grid, we have to know the column currently occupied by it. To do that, the first thing we will do is add a private property, mCurrentColumn
, into the FPlayerPiece
struct:
int32 mCurrentColumn;
And then the two traditional Set()
and Get()
public functions:
void SetCurrentColumn(int32 Column) { mCurrentColumn = Column; }
int32 GetCurrentColumn() const { return mCurrentColumn ;}
Finally, right after spawning the piece, at StateSpawning()
, we set the piece's column:
mPlayerPiece.SpawnPiece([this, &spawn_row, &spawn_col](int32 Index)
{
return SpawnBlock(spawn_col, spawn_row+Index, PickRandomBlock(), false);
});
// Set the correct column
mPlayerPiece.SetCurrentColumn(spawn_col);
...
Back to the FPlayerPiece
struct, we will add to it a public function that we will use to perform the horizontal movement. Yes, that's right, just one function. As arguments, it should receive the horizontal coordinate where the piece should stop as well as the amount of time we want this movement to take:
void SideMove(float HorizontalPosition, float TimeLimit);
As explained, we will first make the column change work and then incorporate the animation. This means that for the moment we won't utilize the TimeLimit
argument. All that we will do in here is iterate through each block composing the player piece and use SetActorLocation()
to horizontally reposition them:
void FPlayerPiece::SideMove(float HorizontalPosition, float TimeLimit)
{
for (ABlock* block : mBlock)
{
FVector location = block->GetActorLocation();
location.X = HorizontalPosition;
block->SetActorLocation(location);
}
}
Now let's jump into our input event function, OnSideMove()
, which is part of the AGameModeInGame
. In there we must perform the exact same check we did in the rotation, that is, if we are in the correct internal state (StatePlaytime
) and for the very same reason, the PlayTime is the only state where there is a player piece that can be controlled by the player. Moving forward, we will also have to reset a timer value in case the mapped button/axis is in neutral position. Then, we check if the time counting "allows" us to perform the movement. Once everything is checked, we obtain the destination coordinate based on the direction of the AxisValue
and finally call our PlayerPiece new function meant to change the column:
void AGameModeInGame::OnSideMove(float AxisValue)
{
// Bail if not in the PlayTime state
if (mCurrentState != &AGameModeInGame::StatePlaytime)
return;
// Also bail if the axis value is 0
if (AxisValue == 0.0f)
{
// Reset the timer if the mapped button is in neutral state
mSideMoveTimer = 0.0f;
}
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;
// The row does not matter in this case since we only want the horizontal coordinate
const float dest_coord = mPlayField->GetCellLocation(dest_col, 0).X;
// Move the piece
mPlayerPiece.SideMove(dest_coord, 0);
// 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);
}
}
This can now be tested (of course, after building the project). We do have horizontal movement of the player piece. However, this "warped movement" is not pretty, right? We have to correct that, although it will require quite a bit of code and some modifications in the input event function.
In order to perform the "animation" we once again will need to count time. And again, we will not make use of the timers provided by Unreal Engine, instead we will manually do that. In this case, for each animation we want to count time we will need two variables, one to count the elapsed time and the other to specify the limit of this counting. In order to make things a little simpler, we will create an struct meant to hold the two reference variables and some basic functionality that won't require a separate file. We already have an .h
file meant to hold this kind of struct, helpers.h
.
Before dumping the code for this new struct, let's see the necessary functionality. We will need means to update the elapsed time held by the object, which will be done by Update()
function. All that will be done in there will be add the argument's value into the internal stored elapsed time
and then return the result divided by the time limit (say, a "percent" of how much time has been elapsed towards the limit). We also need means to set the internal values, however the elapsed time
can just be reset after setting the time limit
through a Set()
function. With this, we don't provide a function to directly set the elapsed time
variable, only to reset it. That's it, there isn't anything else! The code:
USTRUCT()
struct FTiming
{
GENERATED_USTRUCT_BODY()
public:
FTiming()
: mLimit(0.5f)
, mElapsed(0.0f)
{}
void Set(float Limit)
{
mLimit = Limit;
Reset();
}
void Reset() { mElapsed = 0.0f; }
float Update(float DeltaSeconds)
{
mElapsed += DeltaSeconds;
return mElapsed / mLimit;
}
private:
float mLimit;
float mElapsed;
};
Although the limit for the vertical movement will change during the game (remember, the falling speed must increase as the game progresses), we will still need a reference initial value. The horizontal movement will not have many changes but still requires a reference. As usual, we will want to tweak these values in the editor. Because this kind of pattern has already been explained multiple times throughout the tutorial, I will only mention where the code must be added. With that said, in the UColGameInstance
class, private section:
UPROPERTY(EditAnywhere, meta = (DisplayName = "Horizontal Move Time"))
float mHorizontalMoveTime;
UPROPERTY(EditAnywhere, meta = (DisplayName = "Vertical Move Time"))
float mVerticalMoveTime;
The two public functions to obtain those values:
UFUNCTION(BlueprintPure)
float GetHorizontalMoveTime() const { return mHorizontalMoveTime; }
UFUNCTION(BlueprintPure)
float GetVerticalMoveTime() const { return mVerticalMoveTime; }
The initialization in the constructor:
UColGameInstance::UColGameInstance()
{
...
mHorizontalMoveTime = 0.2f;
mVerticalMoveTime = 0.5f;
}
The two "shortcut" functions in the blueprint library:
// Obtain the horizontal move time - amount of time to move a single cell
UFUNCTION(BlueprintPure, meta = (WorldContext = "WorldContextObject"))
static float GetHorizontalMoveTime(const UObject* WorldContextObject);
// Obtain the vertical move time - amount of time to move a single cell
UFUNCTION(BlueprintPure, meta = (WorldContext = "WorldContextObject"))
static float GetVerticalMoveTime(const UObject* WorldContextObject);
Which are implemented like this:
float UColBPLibrary::GetHorizontalMoveTime(const UObject* WorldContextObject)
{
if (UColGameInstance* gi = GetColGameInstance(WorldContextObject))
{
return gi->GetHorizontalMoveTime();
}
return 0.2f;
}
float UColBPLibrary::GetVerticalMoveTime(const UObject* WorldContextObject)
{
if (UColGameInstance* gi = GetColGameInstance(WorldContextObject))
{
return gi->GetVerticalMoveTime();
}
return 0.5f;
}
Early in this part I briefly described how the animation will be done. It's time to delve into some more details of how this will work. As mentioned, we will use the elapsed time to perform an interpolation of the visual position of the player piece. To perform this interpolation, we need two positions, the original position and the destination. Since we can obtain a "percent" of how much time has elapsed, we use that as alpha on the linear interpolation function provided by Unreal Engine.
Based on this, whenever we perform an horizontal movement, we will need to store the original position and the destination position so we can use those to perform the interpolation. With that in mind, let's update our ABlock
class to hold those two reference positions, in the private section:
// Reference position values used to animate movement
FVector mOriginalPosition;
FVector mFinalPosition;
Remember when we implemented the block order changing, where we have to also swap the actor locations? Well, those two vectors directly affect the actor locations, meaning that we will also have to swap them in the SwapWith()
function. So, let's update it, shall we?
void ABlock::SwapWith(ABlock* OtherBlock)
{
...
// Swap the original position
Swap(mOriginalPosition, OtherBlock->mOriginalPosition);
// Swap the final position
Swap(mFinalPosition, OtherBlock->mFinalPosition);
}
We have to count time, right? We even created one little struct to help with this task. Now, whenever we have to update the timing, as well as reset it, it will be done for all blocks of the player piece. This means that for vertical and horizontal movement we can count time from the FPlayerPiece
struct. So, let's update the player piece .h
file to include the helpers.h
file:
#include "CoreMinimal.h"
#include "helpers.h"
#include "PlayerPiece.generated.h"
With this we can now declare the two timing objects in the private section of the player piece "class":
// Animation timing
FTiming mVerticalTime;
FTiming mHorizontalTime;
We will want to update these two timing objects. Shortly I will show how we will do that, but first we have to deal with another thing. At this moment it should be clear that the time limit
for those two objects will most certainly be different, meaning that we will need to calculate two interpolations. Inside the player piece we will always want to calculate the interpolation for the vertical movement, however, horizontal movement may not always be the case. Because of that we need some way to "tell" us if the player piece is horizontally moving or not. The easiest way to do that is through the use of a flag (a boolean, really) that we will change accordingly. With that in mind, let's add a private property into the FPlayerPiece
"class":
// Movement flag
bool mIsSideMoving;
By default this should be false. Granted, we want this to be true only when setting up horizontal movement. What this means is that we set this to false right after spawning the blocks:
template <typename GetBlockFunc>
void SpawnPiece(GetBlockFunc Func)
{
const int32 bcount = mBlock.Num();
for (int32 i = 0; i < bcount; i++)
{
mBlock[i] = Func(i);
}
mIsSideMoving = false;
}
It's also a really good idea to set this to false in the constructor (remember that we have inlined this):
FPlayerPiece()
: mIsSideMoving(false)
{}
It's now time to update the timing objects (please, no comments about this). Typically, this kind of update is done through the Tick()
function. As it turns out, we don't have this function in our FPlayerPiece
struct. Well, let's create one them! In the public section of this "class", we declare it:
void Tick(float DeltaSeconds);
An overview of the tasks of this function:
The major thing is the fact that we need the reference positions (original and destination) in order to perform the interpolation. Of course, we also need the elapsed time percent value to be used as alpha in the interpolation function. One approach would be to retrieve the reference values from the block and then perform everything from our new Tick()
function. A different approach, which is the one we will take, is to actually add/implement those interpolations as functions in the block class. Doing this will give us the possibility to reuse some code when we come into another game rule, where we have to relocate blocks to fill in gaps left by other blocks that have been destroyed. We will then declare and implement those two functions. Once that is done we come back and implement the Tick()
. In the public section of the ABlock
class we declare:
bool InterpolateHorizontal(float Alpha, float& OutCoordinate);
bool InterpolateVertical(float Alpha, float& OutCoordinate);
Notice the return type, bool
? When we calculate the interpolation, if the result actually finishes the movement we return true. Otherwise, we return false. Also, because we need two output values, we obtain the resulting coordinate through a reference in the argument list of the function. Let's see their implementation:
bool ABlock::InterpolateHorizontal(float Alpha, float& OutCoordinate)
{
if (Alpha >= 1.0f)
{
OutCoordinate = mFinalPosition.X;
return true;
}
OutCoordinate = FMath::Lerp<float>(mOriginalPosition.X, mFinalPosition.X, Alpha);
return false;
}
bool ABlock::InterpolateVertical(float Alpha, float& OutCoordinate)
{
if (Alpha >= 1.0f)
{
OutCoordinate = mFinalPosition.Z;
return true;
}
OutCoordinate = FMath::Lerp<float>(mOriginalPosition.Z, mFinalPosition.Z, Alpha);
return false;
}
As you can see, the interpolation is relatively simple. The most important aspect is that if the interpolation alpha has reached (or gone beyond) 1.0 (which means, 100%), we just "set" the resulting position to the one we originally intended to be, otherwise the block could "overshoot" the destination. Alternatively we could just clamp the Alpha value.
Time to go back into our Tick()
function and actually update the timing. Initially we will only work with the horizontal movement since there is still some other extra code needed to correctly implement the vertical movement. In this function, we update the timing objects and then initialize a vector, interp_position
, that will be used to set the block positions. The interpolation functions will directly set the components of this vector. When calling the horizontal interpolation function we verify the return value to properly set the mIsSideMoving
flag. There is also a temporary line, setting the vertical (Z) coordinate. We will substitute that once we get to implement the vertical movement.
void FPlayerPiece::Tick(float DeltaSeconds)
{
// Update the timing objects
const float halpha = mIsSideMoving ? mHorizontalTime.Update(DeltaSeconds) : 0;
const float valpha = mVerticalTime.Update(DeltaSeconds);
// Iterate through each block, interpolating and updating the location
FVector interp_position = mBlock[0]->GetActorLocation();
for (ABlock* block : mBlock)
{
// Temporarily get the Z coordinate - this will be computed correctly later
interp_position.Z = block->GetActorLocation().Z;
// Perform side horizontal movement if necessary
if (mIsSideMoving)
{
if (block->InterpolateHorizontal(halpha, interp_position.X))
{
mIsSideMoving = false;
}
}
// Update the actor location
block->SetActorLocation(interp_position);
}
}
While not fully implementing the vertical movement, there is some code already in place, ready to be used later. Next step, we have to setup the reference positions in the actor class. Generally speaking, the mOriginalPosition
is the "current actor location" when setting up the movement. The mFinalPosition
needs "calculation" and we have already done this for the horizontal coordinate of the vector. Nevertheless, we will perform this setup through the use of 3 functions. The first one will initialize the mOriginalPosition
while the other two are meant to update the relevant components of the vector for both horizontal and vertical movement, respectively. In the public section of the ABlock
class, then, we add these new functions:
// Reference location vectors setup
void InitOriginalPosition() { mOriginalPosition = GetActorLocation(); }
void SetupHorizontal(float Coordinate) { mFinalPosition.X = Coordinate; }
void SetupVertical(float Coordinate) { mFinalPosition.Z = Coordinate; }
Notice that we somewhat "ignored" the Y coordinate in the mFinalPosition
vector for those setup functions. If you take a look again into the code inside Tick()
we do take care of this coordinate, when retrieving the actor location and setting into the interp_position
vector. Moving on, now we have to actually setup the animation, which we can do from the SideMove()
function in the FPlayerPiece
"class". We have some temporary code in there that "warps" the blocks into the desired location. The task of this function is basically set the timing object, set the flag to indicate horizontal movement and then setup the location reference vectors of each block:
void FPlayerPiece::SideMove(float HorizontalPosition, float TimeLimit)
{
mHorizontalTime.Set(TimeLimit);
mIsSideMoving = true;
for (ABlock* block : mBlock)
{
block->InitOriginalPosition();
block->SetupHorizontal(HorizontalPosition);
}
}
There are two last things that must be done before testing what has been done. The first thing is the moment we are calling player piece's SideMove()
function, inside the AGameModeInGame::OnSideMove()
. The second argument of SideMove()
is the time limit which we were passing 0. So, let's update just that line so it looks like this:
mPlayerPiece.SideMove(dest_coord, UColBPLibrary::GetHorizontalMoveTime(this));
The second change is the fact that we have to update the timing, that is, call the Tick()
function we have added to the FPlayerPiece
struct. To do that, we simply call it from the StatePlaytime()
:
AGameModeInGame::StateFunctionProxy AGameModeInGame::StatePlaytime(float Seconds)
{
// Update the blocks within the player piece
mPlayerPiece.Tick(Seconds);
return &AGameModeInGame::StatePlaytime;
}
We can now test the movement. It indeed provides an smooth horizontal movement instead of that weird warping. However, there is an small problem here. If you start moving into one direction and quickly request to move into the other direction, the movement will be noticeable slower. Or, if you repeatedly and quickly press the button to move the piece in a single direction, the piece will move faster than intended. This happens because we are setting the exact same time limit to travel different distances. In order to fix that we will have to compute a fraction of the time limit proportional to the distance that must be changed.
To calculate the time limit fraction we will need some information:
We already have means to obtain the time needed for a "full cell" movement, GetHorizontalMoveTime()
and GetVerticalMoveTime()
, for horizontal and vertical movements respectively. We sort of have the full cell distance, just not available (yet) to be used where we need. The last piece of information can be obtained by simple computing the difference between current coordinate and the destination coordinate. The overall ideal then is to calculate the proportion (a percent, really) of the traveled distance in relation to the size of a cell and then multiply that by the time limit for a full cell travel.
In math formula, if we take to be the distance to be traveled, the scaled size of a cell, the "unit time" (that is, the time needed to move a full cell) and the resulting time limit, then we can calculate :
Again, we already have means to obtain . Our APlayField
class can provide us , so let's add a public function meant to do that. Its operation is really easy since all we have to do is multiply the mMapScale
by the mTileSpriteSize
:
float GetScaledCellSize() const { return mTileSpriteSize * mMapScale; }
In this function, notice that it's possible to cache the result value in order to avoid some possible unnecessary multiplications. The thing is, IF the window is resized, mMapScale
will get a different value and it will be necessary to recalculate the cached value. In order to make things a lot simpler, I will maintain the code as shown. Moreover, it's really doubtful this code will cause any negative impact in the performance of the game, so...
We now have to calculate . As said, it's jut a coordinate difference (subtraction), however between the desired destination and the current (at the setup time) position. When we call the SideMove()
function of the Player Piece we do specify de destination coordinate, meaning that we can directly use that value. Also, the player piece already contains access to the blocks, so current position can be easily obtained. In the player piece we can create two public functions meant to return the desired distance difference, one for the horizontal and the other for the vertical:
float GetHorizDiff(float Dest) const;
float GetVertDiff(float Dest) const;
Before showing the code for those two functions (which are one-line) I have to explain something. On both I make use of only the block stored in the index 0 of the mBlock
array. For horizontal movement it should be easy to understand since all blocks of the player piece should be aligned in a column, occupying the exact same horizontal (X) coordinate, changing only the vertical (Z) coordinate between them. When dealing with the vertical difference, the reason for only using the first block in the array is the fact that all blocks will travel the exact same distance. When the bottom most block (index 0 of the array) reaches the "floor", all of the above blocks will also have landed, one on top of the other. The code:
float FPlayerPiece::GetHorizDiff(float Dest) const
{
return FMath::Abs(Dest - mBlock[0]->GetActorLocation().X);
}
float FPlayerPiece::GetVertDiff(float Dest) const
{
return FMath::Abs(Dest - mBlock[0]->GetActorLocation().Z);
}
The last thing, update the input handling, where we call the player piece's SideMove()
function. In there we just calculate the time limit fraction and use the result as the second argument to the SideMove()
:
void AGameModeInGame::OnSideMove(float AxisValue)
{
...
else
{
...
// The row does not matter in this case since we only want the horizontal coordinate
const float dest_coord = mPlayField->GetCellLocation(dest_col, 0).X;
// Calculate the time limit fraction
const float tfraction = mPlayerPiece.GetHorizDiff(dest_coord) / mPlayField->GetScaledCellSize() * UColBPLibrary::GetHorizontalMoveTime(this);
// Move the piece
mPlayerPiece.SideMove(dest_coord, tfraction);
...
}
}
Testing now shows that we indeed fixed the issue of moving slower or faster. However, the horizontal movement of the piece seems slightly slow. Instead of changing the input delay, let's change the animation time limit. Inside the game instance blueprint we edit the Horizontal Move Time
property. By default we have set this to 0.2. In order to make the movement faster we have to decrease the time to complete the movement, so let's experiment 0.15. This value seems reasonable, although if you maintain the button pressed you may not like the resulting "move-stop-move" pattern. This happens because of the input delay timer being longer than the time needed to complete the horizontal movement. Remember, however, that if we set the input timer to allow faster repeat of the movement command then it will become harder to position the player piece. Feel free to tweak those values until you find the ones that you like! Nevertheless, for reference, I leave here the two values I have left in my project files: Side Move Delay = 0.16
and Horizontal Move Time = 0.15
.
With quite a bit of code we have now finished almost all of the player interaction that can be performed on top of the player piece, missing only the "accelerate" input! The horizontal movement is happening with the "animation", that is, smoothly. We did also include some preparatory code for the vertical movement (the piece falling down). I have left this to the next part mostly because we will have to incorporate some extra code to correctly detect where the piece will end its movement at the column occupied by it.