Debugging UI bug in my game

Hi, I’m currently developing a “Mario Party” style of game. I’m making good progress, but I’m experiencing a bug with the UI.

My game board currently consists of a starting space for the player characters and three spaces I’ve designated to give you 5 coins.
Currently, here’s how the UI updates:

  1. After the first move, the player listed on the top-right of the UI has their coins set to 5.
  2. After the second, the player listed on the bottom-left of the UI has their coins set to 10.
  3. After the third move, the player listed on the bottom-right has their coins set to 15.

What I want to achieve is the behavior generally expected of games of this type – when the first player moves onto an “Add Coins” space the top-left coins counter increases by the specified value, when the second moves the top-right counter increases, etc.

The below BP is on the UI. PlayerUIBlock 0 corresponds to the top-left UI, 1 is the top-right, and so on:


SetPlayerUI is defined as:

PartyPlayerState is a straightforward C++ class that has int properties for SpecialCoins and Coins, and getters and setters for them.

Let me know what other code samples you might need. Thanks.

After investigating the actual coin values, it seems they are only set for the first player (according to its player state). On the first turn the player gets 5 coins, they get another 5 on the next, with a total of 15 coins after three turns.

I’m still not sure how that works with the code I wrote above, though.

Have you tried printing the names of the returned actor for each Get?

1 Like

Yes. It seems to be looping through all of the names just fine.

image

I’m calling it from GetAllPlayers:

TArray<APartyCharacter*> AFourPlayerMatchGameState::GetAllPlayers()
{
	for (int i = 0; i < Players.Num(); i++)
	{
		UE_LOG(LogTemp, Warning, TEXT("Player %d: %s"), i, *Players[i]->GetName());
	}
	return Players;
}

I did notice something I suspect could be the culprit. I tried to output the values of the associated Players in my PartyPlayerState class in the Tick function. It seems on game start the values are correctly populated. For example, this looks fine:


However, after every turn, a “Party Character” value is removed from one of the Player States. It seems to be in the order that players move around the map. I’m not sure what could be causing this, though.

I can send you a copy of my game, if you would like.

What exactly do you mean by that. How do you do that?

As a last ditch effort If I run out of ideas, sure. As you would probably have to fill me in on the whole setup and the workings of it (or figuring that out myself).

1 Like

I’m referring to the general course of the game – the player rolls their die and they move to a different space in the level.

I’ll try to be more clear:

For my game, I have four different Player States: one for each player.


Each of the Player States has a reference to one of the player Pawns. For debugging purposes I have created a variable PartyCharacter that has the property VisibleAnywhere in the class to hold a reference to the Player State’s associated pawn.

void APartyPlayerState::Tick(float DeltaTime)
{
	PartyCharacter = Cast<APartyCharacter>(GetPawn());
}

As an example, at game start BP_PartyPlayerState3 has a value set in its PartyCharacter variable, such as BP_PartyCharacter3. I took this screenshot before any of the players moved.

However, after the player takes their turn by rolling a die and having their game piece move to a given spot, this Player State variable is erroneously being cleared out. Here’s the same instance of the game as before, but after BP_PartyCharacter3 moves the Player State’s PartyCharacter variable is cleared out.

I hope this explanation makes more sense.

That does help though I was mainly asking about what constitutes as “move around the map”; I was thinking you would have different maps for the minigames and would think that maybe the playerstate isn’t persistent across travel.

I see that you have that code in Tick; is there a specific reason for that? Were you working around some other issue as if you were it could be related.

1 Like

I wrote that code to investigate the issue with the coin values not appearing in the UI, since the PlayerState relies on the actions made by the PartyCharacter to determine whether its values should be updated (e.g. if the PartyCharacter moves on to an AddCoinSpace, the space gets the PartyCharacter's PlayerState to add coins).

Perhaps incidentally, here is the function I wrote to add coins to the PlayerState:

void AAddCoinSpace::PerformAction(APartyCharacter* AddedCharacter)
{
	APartyPlayerState* State = Cast<APartyPlayerState>(AddedCharacter->GetPlayerState());

	State->SetCoins(State->GetCoins() + CoinAmountToAdd);
}

Then I think I would need to take a look at your project because I’m out of ideas.

1 Like

Okay, I’ve sent it over through here. (I added some missing data table sources in a separate support request).

Here is some info about the game design. There are some idiosyncrasies that may be confusing.

  • Currently, the game only supports a single input device; you can either click or press the lower face button on a gamepad, and that same device is used for all four players.

  • So far, after the player rolls the die and moves, the camera stays on that moved player. Rolling the die again rolls for the next player and the camera then focuses on them.

In regards to my code structure:

  • When the game starts, AFourPlayerMatchGameMode::BeginPlay() is executed. One of the things it does is call SpawnPlayers(TArray<AActor*> PlayerStartsInWorld) to spawn the player characters (SetAllPlayers() is unused)

  • AFourPlayerMatchGameMode::StartMidGame is executed by a delegate in the DialogueManager after the game finishes with its opening dialogue.

Let me know if you need any more info. I appreciate you helping me out.

So I’m a little confused here.

What steps do I need to take to see the problem? What should I be looking at?

1 Like

If you start the game and progress past the dialogue (by pressing left-click), you can see the player starting order.

The player with the smallest number rolls first, and the order is ascending. They roll by pressing left click. Afterward, they are moved to a blue space, after which their coin score is supposed to be incremented by 5.

Suppose you start the game, get past the dialogue, and now you’re on the first move. Left-clicking moves Player 1. However, instead of the top-left coin UI increasing to 5, it is the top-right, which is supposed to correspond to the second player.


(I took this image after my first move).

Furthermore, the next two moves set the bottom-left and bottom-right coin values to 10 and 15, respectively. The second move should have been the one to set the top-right coin value to 5, and the third should have set the bottom-left coin value to 5.

Some potential avenues for examination might include the “AllCharacterHUD” Widget Blueprint in the UI folder. You can see from the Graph view I am attempting to set the coin values for the UI. Another might be the SpawnPlayers function in the FourPlayerMatchGameMode.cpp file; this is where player characters 2-4 are spawned in-game and are assigned their Player States.

I hope that was clear.

So GetAllPlayers isn’t returning the players in the same order so you can’t rely on index 0 being player 0. You should store the player states of each character once at the beginning. Then use that for the text; so in the end you would have

1 Like

Can you clarify why the players aren’t being returned in the same order?

The Players array is defined in FourPlayerMatchState. In the function AFourPlayerMatchGameMode::SetPlayerOrderFromStartingDice(), which is called from BeginPlay(), I sort the players in the array based on the values of their dice from least to greatest. If you set a breakpoint at the end of the function it appears that the players are in fact in the correct order (you can check the die value on each player’s UDie * object).

The GetAllPlayers() function simply returns the Players array and outputs it to the debug log.

TArray<APartyCharacter*> AFourPlayerMatchGameState::GetAllPlayers()
{
	for (int i = 0; i < Players.Num(); i++)
	{
		UE_LOG(LogTemp, Warning, TEXT("Player %d: %s"), i, *Players[i]->GetName());
	}
	return Players;
}

What is causing GetAllPlayers to return the array values in a different order than expected?

Sorry I misremembered and also didn’t dive in too much to see what was happening.

I was printing the index and player state name and IIRC they didn’t match up. A quick change to store and just use the states directly seemed to fix it and I didn’t have enough time to investigate why.

1 Like

That’s fair.

I just tried your approach and I’m getting the same behavior as before, with the first player increasing the second player’s coin UI, and so on. I think your use of the states directly is good though so I’ll use it instead.

If you look over at the PlayerState instances (I have them as variables on the player characters during play), you can see that after each turn, one of those states is being set to null. I suspect determining why that is happening would go a long way towards understanding the problem. Some sort of pointer error maybe.

The player state is part of the player controller. So you’re never changing that; in ChangeCurrentPlayer before Unpossess and after Possess:

Before:
    PlayerController: BP_PartyPlayerController_C_0
    Pawn: BP_PartyCharacter_C_2
    State: BP_PartyPlayerState_C_0
After:
    PlayerController: BP_PartyPlayerController_C_0
    Pawn: BP_PartyCharacter_C_1
    State: BP_PartyPlayerState_C_0

Before:
    PlayerController: BP_PartyPlayerController_C_0
    Pawn: BP_PartyCharacter_C_1
    State: BP_PartyPlayerState_C_0
After:
    PlayerController: BP_PartyPlayerController_C_0
    Pawn: BP_PartyCharacter_C_0
    State: BP_PartyPlayerState_C_0

Before:
    PlayerController: BP_PartyPlayerController_C_0
    Pawn: BP_PartyCharacter_C_0
    State: BP_PartyPlayerState_C_0
After:
    PlayerController: BP_PartyPlayerController_C_0
    Pawn: BP_PartyCharacter_C_3
    State: BP_PartyPlayerState_C_0

Before:
    PlayerController: BP_PartyPlayerController_C_0
    Pawn: BP_PartyCharacter_C_3
    State: BP_PartyPlayerState_C_0
After:
    PlayerController: BP_PartyPlayerController_C_0
    Pawn: BP_PartyCharacter_C_2
    State: BP_PartyPlayerState_C_0
1 Like

Interesting. I thought the PlayerController's PlayerState was inconsequential.

In SpawnPlayers I’m creating a new player state for each player. APawn has a built-in function SetPlayerState() to initialize the player states for the given Pawn. In the AddCoinSpace class I am calling GetPlayerState() on my pawn.

So, are you stating that in my scenario, storage and retrieval of the player state does not work with the pawn – just the player controller?

Disclaimer: I’ve not used player states before and barely touched multiplayer so I’m kind of learning as we go here

They point to the same state. When you call Possess it will set the possessed pawn’s state to the player state the player controller is pointing to. Similarly when you call Unpossess it will set the to-be-unpossessed-pawn’s player state to nullptr.

image
image

So I don’t think you can do this with a single player controller.


Feel free to ignore this unsolicited code feedback:

In the game state you have:

UFUNCTION(BlueprintPure)
TArray<class APartyCharacter*> GetAllPlayers();

This is creating a new array each time as you are returning this by value. As its just returning a data member it should return a reference instead, you may want const and non const versions. i.e.

TArray<class APartyCharacter*>& GetAllPlayers();
const TArray<class APartyCharacter*>& GetAllPlayers() const;

With that said, do you really need to expose this data? I have only seen code around where I’ve added logs so from the looks of it from there you’re only using it to then just get certain players, so it seems that would be the better interface to expose

UFUNCTION(BlueprintPure)
class APartyCharacter* GetPlayer(int32 Index); // returns Players[Index];

doing that should tidy up the blueprint a bit.

You also have CurrentPlayer and currentPlayerIndex (inconsistent casing), you only need to store one or the other, it’s also a little confusing as a quick FindAllReferences, you’re only using CurrentPlayer in BeginPlay?

ChangeCurrentPlayer should probably be called AdvanceCurrentPlayer as the current name suggests you can change it to an arbitrary player. It also has a redundant first parameter and doesn’t need the out parameter. As you could can just expose a function for that

bool AFourPlayerMatchGameState::IsRoundDone() // or StartNewRound
{
    return CurrentPlayerIndex == 0;
}

Also to advance the index you can use the modulus operator

CurrentPlayerIndex = (CurrentPlayerIndex + 1) % Players.Num(); 
1 Like

Interesting! Unpossess() must be the cause of why each Player State is being set to null during play – the player moves, Unposess() is executed on the controller, and, as you said, the pawn itself is getting its player state set to null.

Part of the reason I had set my game to use one PlayerController was due to a lack of physical controllers to test the game. I wasn’t sure if I could change a given input device’s PlayerController after each turn, so I might not have been able to change player characters in-game.

If I could have one PlayerController, therefore, I wouldn’t have to worry about physically obtaining four input devices; I thought I could just unpossess one pawn and possess another, and I only would need one input device.

Privacy & Terms