Start of game mode

This commit is contained in:
Kevin Poretti 2023-01-08 17:33:25 -05:00
parent 8ac4271602
commit 1fc53e9e62
16 changed files with 941 additions and 18 deletions

View File

@ -1,3 +1,8 @@
[/Script/EngineSettings.GeneralProjectSettings]
ProjectID=EAE41D29471FBD447E5454B1D01C9ECF
ProjectName=Third Person Game Template
ProjectName=Gravity Stomp
Description=Multiplayer competitive platformer where you manipulate gravity
CopyrightNotice=Gravity Stomp Copyright Kevin Poretti
ProjectVersion=0.0.1
ProjectDisplayedTitle=NSLOCTEXT("[/Script/EngineSettings]", "79D56CC04C9E98AB5B2981808A14FB31", "Gravity Stomp")

Binary file not shown.

BIN
GravityStomp/Content/Maps/Debug/Test.umap (Stored with Git LFS)

Binary file not shown.

View File

@ -1,4 +1,4 @@
// Copyright Epic Games, Inc. All Rights Reserved.
// Gravity Stomp Copyright Kevin Poretti
#include "GSCharacter.h"
#include "Camera/CameraComponent.h"
@ -57,6 +57,11 @@ AGSCharacter::AGSCharacter(const FObjectInitializer& ObjectInitializer)
}
void AGSCharacter::ChangeTeamColor(bool bIsPlayerFriendly)
{
}
void AGSCharacter::BeginPlay()
{
// Call the base class
@ -98,6 +103,7 @@ void AGSCharacter::Move(const FInputActionValue& Value)
AddMovementInput(FVector::RightVector, MovementVector.X);
}
void AGSCharacter::ChangeGravityDirection(const FInputActionValue& Value)
{
FVector2D GravityDirection = Value.Get<FVector2D>();

View File

@ -1,4 +1,4 @@
// Copyright Epic Games, Inc. All Rights Reserved.
// Gravity Stomp Copyright Kevin Poretti
#pragma once
@ -35,6 +35,8 @@ class AGSCharacter : public ACharacter
public:
AGSCharacter(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get());
void ChangeTeamColor(bool bIsPlayerFriendly);
protected:
/** Called for movement input */

View File

@ -1,4 +1,4 @@
// Fill out your copyright notice in the Description page of Project Settings.
// Gravity Stomp Copyright Kevin Poretti
#include "Character/GSCharacterMovementComponent.h"
@ -856,15 +856,9 @@ void UGSCharacterMovementComponent::PhysicsRotation(float DeltaTime)
if (ShouldRemainVertical())
{
FString DebugDesiredRotation = FString::Printf(TEXT("Desired Rotation: %s"), *DesiredRotation.ToString());
GEngine->AddOnScreenDebugMessage(-1, 0.0f, FColor::Cyan, DebugDesiredRotation);
DesiredRotation.Pitch = IsCharacterUpAlignedToWorldUp() ? 0.f : FRotator::NormalizeAxis(DesiredRotation.Pitch);
DesiredRotation.Yaw = IsCharacterUpAlignedToWorldUp() ? FRotator::NormalizeAxis(DesiredRotation.Yaw) : 0.f;
DesiredRotation.Roll = GetRollFromCharacterUpDir(CharacterUpDirection);
DebugDesiredRotation = FString::Printf(TEXT("Desired Rotation after keeping it vertical: %s"), *DesiredRotation.ToString());
GEngine->AddOnScreenDebugMessage(-1, 0.0f, FColor::Cyan, DebugDesiredRotation);
}
else
{

View File

@ -1,4 +1,4 @@
// Fill out your copyright notice in the Description page of Project Settings.
// Gravity Stomp Copyright Kevin Poretti
#pragma once

View File

@ -1,7 +1,309 @@
// Copyright Epic Games, Inc. All Rights Reserved.
// Gravity Stomp Copyright Kevin Poretti
#include "GSGameModeBase.h"
// GS includes
#include "EngineUtils.h"
#include "GSGameState.h"
#include "Player/GSPlayerController.h"
#include "Player/GSPlayerState.h"
AGSGameModeBase::AGSGameModeBase()
{
ScoreLimit = 25;
MaxNumTeams = 2;
TimeBetweenRounds = 20.0f;
RoundTime = 600.0f;
WarmupTime = 15.0f;
bWarmupEnabled = true;
bDelayedStart = bWarmupEnabled;
PlayerControllerClass = AGSPlayerController::StaticClass();
PlayerStateClass = AGSPlayerState::StaticClass();
GameStateClass = AGSGameState::StaticClass();
MinRespawnDelay = 5.0f;
}
void AGSGameModeBase::PreInitializeComponents()
{
Super::PreInitializeComponents();
// Setup timer for match tick
GetWorldTimerManager().SetTimer(TimerHandle_DefaultTimer, this, &AGSGameModeBase::DefaultTimer, GetWorldSettings()->GetEffectiveTimeDilation(), true);
}
void AGSGameModeBase::InitGame(const FString& MapName, const FString& Options, FString& ErrorMessage)
{
Super::InitGame(MapName, Options, ErrorMessage);
bUseSeamlessTravel = !GetWorld()->IsPlayInEditor();
}
void AGSGameModeBase::InitGameState()
{
Super::InitGameState();
AGSGameState* GS = GetGameState<AGSGameState>();
check(GS)
GS->SetNumTeams(MaxNumTeams);
}
void AGSGameModeBase::PreLogin(const FString& Options, const FString& Address, const FUniqueNetIdRepl& UniqueId,
FString& ErrorMessage)
{
// reject player is match has ended
AGSGameState* GS = GetGameState<AGSGameState>();
bool bMatchEnded = GS && GS->HasMatchEnded();
if(bMatchEnded)
{
ErrorMessage = TEXT("Can't join a match that has ended.");
}
else
{
Super::PreLogin(Options, Address, UniqueId, ErrorMessage);
}
}
void AGSGameModeBase::PostLogin(APlayerController* NewPlayer)
{
Super::PostLogin(NewPlayer);
PickTeam(NewPlayer->GetPlayerState<AGSPlayerState>());
AGSPlayerController* PC = Cast<AGSPlayerController>(NewPlayer);
if(PC && IsMatchInProgress())
{
PC->Client_GameStarted();
}
}
void AGSGameModeBase::RestartPlayer(AController* NewPlayer)
{
Super::RestartPlayer(NewPlayer);
// inform player controller that game has started using client RPC
AGSPlayerController* PC = Cast<AGSPlayerController>(NewPlayer);
if(PC)
{
PC->Client_GameStarted();
}
}
bool AGSGameModeBase::ShouldSpawnAtStartSpot(AController* Player)
{
return false;
}
bool AGSGameModeBase::CanDamagePlayer(AGSPlayerController* Damager, AGSPlayerController* Victim)
{
AGSPlayerState* DamagerPS = Damager ? Damager->GetPlayerState<AGSPlayerState>() : nullptr;
AGSPlayerState* VictimPS = Victim ? Victim->GetPlayerState<AGSPlayerState>() : nullptr;
// can damage if both instigator and victim are valid and are on different teams
return DamagerPS && VictimPS && (DamagerPS->GetTeamNum() != VictimPS->GetTeamNum());
}
void AGSGameModeBase::PickTeam(AGSPlayerState* PlayerState)
{
// place player on the team with the fewest players, or on the first team if all teams have the same number of players
TArray<int32> NumTeamPlayers;
AGSGameState* GS = GetGameState<AGSGameState>();
NumTeamPlayers.AddZeroed(GS->GetNumTeams());
check(NumTeamPlayers.Num() > 0);
for(int32 PlayerIdx = 0; PlayerIdx < GS->PlayerArray.Num(); ++PlayerIdx)
{
AGSPlayerState* CurrPS = Cast<AGSPlayerState>(GS->PlayerArray[PlayerIdx]);
if(CurrPS && CurrPS->GetTeamNum() >= 0 && CurrPS->GetTeamNum() < NumTeamPlayers.Num())
{
NumTeamPlayers[CurrPS->GetTeamNum()]++;
}
else
{
UE_LOG(LogTemp, Error, TEXT("Player %s is on team number %d but only %d teams exist"), *CurrPS->GetPlayerName(), CurrPS->GetTeamNum(), NumTeamPlayers.Num())
}
}
int32 LowestTeamIdx = 0;
for(int32 TeamIdx = 0; TeamIdx < NumTeamPlayers.Num(); TeamIdx++)
{
if(NumTeamPlayers[TeamIdx] < NumTeamPlayers[LowestTeamIdx])
{
LowestTeamIdx = TeamIdx;
}
}
PlayerState->SetTeamNum(LowestTeamIdx);
}
void AGSGameModeBase::OnPlayerKilled(AGSPlayerController* Killer, AGSPlayerController* Victim)
{
// Only keep track of kills in an active round
if(MatchState != MatchState::InProgress)
{
return;
}
AGSPlayerState* KillerPS = Killer ? Killer->GetPlayerState<AGSPlayerState>() : nullptr;
AGSPlayerState* VictimPS = Victim ? Victim->GetPlayerState<AGSPlayerState>() : nullptr;
if(KillerPS && VictimPS)
{
// keep track of player's K/D
KillerPS->IncrementNumKills();
Killer->Client_OnKill(KillerPS, VictimPS);
VictimPS->IncrementNumDeaths();
// inform all players of the death
for(FConstPlayerControllerIterator It = GetWorld()->GetPlayerControllerIterator(); It; ++It)
{
AGSPlayerController* PC = Cast<AGSPlayerController>(*It);
if(PC)
{
PC->Client_OnDeath(KillerPS, VictimPS);
}
}
// give killer's team a point and check if score limit is reached
AGSGameState* GS = GetGameState<AGSGameState>();
check(GS);
int32 NewScore = GS->GiveTeamPoint(KillerPS->GetTeamNum());
if(NewScore >= ScoreLimit)
{
FinishMatch();
}
}
}
void AGSGameModeBase::DefaultTimer()
{
AGSGameState* GS = GetGameState<AGSGameState>();
if(GS && GS->GetRemainingTime() > 0)
{
GS->SetRemainingTime(GS->GetRemainingTime() - 1);
if(GS->GetRemainingTime() <= 0)
{
if(GetMatchState() == MatchState::WaitingPostMatch)
{
RestartGame();
}
else if (GetMatchState() == MatchState::InProgress)
{
FinishMatch();
}
else if (GetMatchState() == MatchState::WaitingToStart)
{
StartMatch();
}
}
}
}
void AGSGameModeBase::FinishMatch()
{
if(IsMatchInProgress())
{
EndMatch();
// determine match winner
AGSGameState* GS = GetGameState<AGSGameState>();
check(GS);
int32 WinningTeamIdx = GS->GetWinningTeam();
// notify players that game has ended and notify winning players that they won
for(FConstPlayerControllerIterator PCIt = GetWorld()->GetPlayerControllerIterator(); PCIt; ++PCIt)
{
// notify player if they won
APlayerController* PC = PCIt->Get();
PC->GameHasEnded(nullptr, PC->GetPlayerState<AGSPlayerState>()->GetTeamNum() == WinningTeamIdx);
}
// turn off all pawns
for (APawn* Pawn : TActorRange<APawn>(GetWorld()))
{
Pawn->TurnOff();
}
// set game state remaining time to time between matches
GS->SetRemainingTime(TimeBetweenRounds);
GS->ResetTeamScores();
}
}
void AGSGameModeBase::HandleMatchIsWaitingToStart()
{
Super::HandleMatchIsWaitingToStart();
// setup warmup
AGSGameState* GS = GetGameState<AGSGameState>();
if(GS && GS->GetRemainingTime() <= 0)
{
if(bWarmupEnabled)
{
GS->SetRemainingTime(WarmupTime);
}
}
// notify players warmup period has started
for(FConstPlayerControllerIterator It = GetWorld()->GetPlayerControllerIterator(); It; ++It)
{
AGSPlayerController* PC = Cast<AGSPlayerController>(*It);
if(PC)
{
PC->Client_WarmupStarted();
}
}
}
void AGSGameModeBase::HandleMatchHasStarted()
{
Super::HandleMatchHasStarted();
// setup in progress match
AGSGameState* GS = GetGameState<AGSGameState>();
if(GS && GS->GetRemainingTime() <= 0)
{
GS->SetRemainingTime(RoundTime);
}
// notify players match has started
for(FConstPlayerControllerIterator It = GetWorld()->GetPlayerControllerIterator(); It; ++It)
{
AGSPlayerController* PC = Cast<AGSPlayerController>(*It);
if(PC)
{
PC->Client_GameStarted();
}
}
}
void AGSGameModeBase::RestartGame()
{
// notify players game has restarted
for(FConstPlayerControllerIterator It = GetWorld()->GetPlayerControllerIterator(); It; ++It)
{
AGSPlayerController* PC = Cast<AGSPlayerController>(*It);
if(PC)
{
PC->Client_GameRestarted();
}
}
Super::RestartGame();
}

View File

@ -1,18 +1,129 @@
// Copyright Epic Games, Inc. All Rights Reserved.
// Gravity Stomp Copyright Kevin Poretti
#pragma once
// GS includes
#include "Player/GSPlayerController.h"
#include "Player/GSPlayerState.h"
// UE includes
#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "GameFramework/GameMode.h"
#include "GSGameModeBase.generated.h"
/**
* Game mode for a team deathmatch/free for all game mode
*/
UCLASS(minimalapi)
class AGSGameModeBase : public AGameModeBase
class AGSGameModeBase : public AGameMode
{
GENERATED_BODY()
public:
AGSGameModeBase();
/**
* Sets up default timer
*/
virtual void PreInitializeComponents() override;
/**
* Initialize the game. This is called before actors' PreInitializeComponents.
*
*/
virtual void InitGame(const FString& MapName, const FString& Options, FString& ErrorMessage) override;
/**
* Initialize the game state. Sets the number of teams based on this game mode's settings.
*/
virtual void InitGameState() override;
/**
* Accept or reject a player attempting to join the server. Fails login if you set the ErrorMessage to a non-empty string.
*/
virtual void PreLogin(const FString& Options, const FString& Address, const FUniqueNetIdRepl& UniqueId, FString& ErrorMessage) override;
/**
* Picks a player team and informs them to start if the match is in progress
*/
virtual void PostLogin(APlayerController* NewPlayer) override;
/**
* Tries to spawn the player's pawn
*/
virtual void RestartPlayer(AController* NewPlayer) override;
/**
* Choose a random spawn
*/
virtual bool ShouldSpawnAtStartSpot(AController* Player) override;
/**
* Checks if one player can damage another. For example, players on the same team should not be able to damage one another.
*/
virtual bool CanDamagePlayer(AGSPlayerController* Damager, AGSPlayerController* Victim);
/**
* Decides on a team for a player based on current player distribution
*/
virtual void PickTeam(AGSPlayerState* PlayerState);
/**
* Handles updating score and informing appropriate clients when one player kills another
*/
virtual void OnPlayerKilled(AGSPlayerController* Killer, AGSPlayerController* Victim);
/**
* Decides which team wins and then informs players the match has ended and if they won or not
*/
void FinishMatch();
/**
* Sets up match warmup timer
*/
virtual void HandleMatchIsWaitingToStart() override;
/**
* Sets up match round timer
*/
virtual void HandleMatchHasStarted() override;
/**
* Notifies players
*/
virtual void RestartGame() override;
/**
* Function that acts as the game mode "tick" and does match state transition logic
*/
virtual void DefaultTimer();
protected:
// Maximum number of teams allowed in this gamemode
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=Settings, meta = (ClampMin = 0))
int32 MaxNumTeams;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=Settings, meta = (ClampMin = 0))
int32 ScoreLimit;
// time in between rounds, in seconds
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=Settings, meta = (ClampMin = 0))
float TimeBetweenRounds;
// duration of a round, in seconds
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=Settings, meta = (ClampMin = 0))
float RoundTime;
// duration of the pre-match/warmup period, in seconds
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=Settings, meta = (ClampMin = 0))
float WarmupTime;;
// whether or not there is a warmup or the round should immediately start
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=Settings)
bool bWarmupEnabled;
FTimerHandle TimerHandle_DefaultTimer;
};

View File

@ -0,0 +1,96 @@
// Gravity Stomp Copyright Kevin Poretti
#include "GameModes/GSGameState.h"
// UE includes
#include "Net/UnrealNetwork.h"
void AGSGameState::SetNumTeams(int32 InNumTeams)
{
NumTeams = InNumTeams;
// Grow team score array if num teams is greater than current capacity
if(TeamScores.Num() < NumTeams)
{
TeamScores.AddZeroed(NumTeams - TeamScores.Num());
}
}
int32 AGSGameState::GiveTeamPoint(int32 InTeamIdx)
{
if(InTeamIdx < 0 || InTeamIdx >= TeamScores.Num())
{
UE_LOG(LogTemp, Error, TEXT("Tried to give team %d a point but the team index is out of range"), InTeamIdx);
return -1;
}
TeamScores[InTeamIdx]++;
OnTeamScoresUpdated.Broadcast(TeamScores);
return TeamScores[InTeamIdx];
}
void AGSGameState::ResetTeamScores()
{
for(int32 TeamIdx = 0; TeamIdx < TeamScores.Num(); TeamIdx++)\
{
TeamScores[TeamIdx] = 0;
}
OnTeamScoresUpdated.Broadcast(TeamScores);
}
int32 AGSGameState::GetWinningTeam()
{
int32 WinningTeamIdx = 0;
int32 WinningTeamScore = 0;
for(int32 TeamIdx = 0; TeamIdx < TeamScores.Num(); TeamIdx++)\
{
if(TeamScores[TeamIdx] > WinningTeamScore)
{
WinningTeamIdx = TeamIdx;
WinningTeamScore = TeamScores[TeamIdx];
}
}
return WinningTeamIdx;
}
void AGSGameState::SetRemainingTime(float InRemainingTime)
{
RemainingTime = InRemainingTime;
OnRemainingTimeUpdated.Broadcast(RemainingTime);
}
void AGSGameState::OnRep_MatchState()
{
Super::OnRep_MatchState();
OnMatchStateUpdated.Broadcast(GetMatchState());
}
void AGSGameState::OnRep_TeamScores()
{
OnTeamScoresUpdated.Broadcast(TeamScores);
}
void AGSGameState::OnRep_RemainingTime()
{
OnRemainingTimeUpdated.Broadcast(RemainingTime);
}
void AGSGameState::GetLifetimeReplicatedProps(TArray< FLifetimeProperty > & OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(AGSGameState, NumTeams);
DOREPLIFETIME(AGSGameState, TeamScores);
DOREPLIFETIME(AGSGameState, RemainingTime);
}

View File

@ -0,0 +1,73 @@
// Gravity Stomp Copyright Kevin Poretti
#pragma once
// UE includes
#include "CoreMinimal.h"
#include "GameFramework/GameState.h"
#include "GSGameState.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FTeamScoresUpdatedSignature, const TArray<int32>&, TeamScores);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FRemainingTimeUpdatedDelegate, float, RemainingTime);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FMatchStateUpdatedSignature, FName, MatchState);
/**
* Game state for a team deathmatch/free for all game mode
*/
UCLASS()
class GRAVITYSTOMPGAME_API AGSGameState : public AGameState
{
GENERATED_BODY()
public:
void SetNumTeams(int32 InNumTeams);
FORCEINLINE int32 GetNumTeams() { return NumTeams; }
/**
* Gives team of the supplied team index a point. Returns the new score for that team.
*/
int32 GiveTeamPoint(int32 InTeamIdx);
void ResetTeamScores();
int32 GetWinningTeam();
FORCEINLINE float GetRemainingTime() { return RemainingTime; }
void SetRemainingTime(float InRemainingTime);
// Begin AGameState interface
virtual void OnRep_MatchState() override;
// End AGameState interface
protected:
// number of teams currently in the game
UPROPERTY(Replicated, BlueprintReadOnly)
int32 NumTeams;
// array of team scores, index by the team number
UPROPERTY(ReplicatedUsing=OnRep_TeamScores, BlueprintReadOnly)
TArray<int32> TeamScores;
// time remaining for the current match state (i.e. warmup, the round itself, post round)
UPROPERTY(ReplicatedUsing=OnRep_RemainingTime, BlueprintReadOnly)
float RemainingTime;
/** Triggered when this weapon is fired. */
UPROPERTY(BlueprintAssignable, Category="Events")
FTeamScoresUpdatedSignature OnTeamScoresUpdated;
UPROPERTY(BlueprintAssignable, Category="Events")
FRemainingTimeUpdatedDelegate OnRemainingTimeUpdated;
UPROPERTY(BlueprintAssignable, Category="Events")
FMatchStateUpdatedSignature OnMatchStateUpdated;
UFUNCTION()
void OnRep_TeamScores();
UFUNCTION()
void OnRep_RemainingTime();
};

View File

@ -1,4 +1,4 @@
// Copyright Epic Games, Inc. All Rights Reserved.
// Gravity Stomp Copyright Kevin Poretti
#pragma once

View File

@ -0,0 +1,68 @@
// Gravity Stomp Copyright Kevin Poretti
#include "Player/GSPlayerController.h"
void AGSPlayerController::Client_GameStarted_Implementation()
{
SetIgnoreMoveInput(false);
// enable all actions on pawn
OnGameStarted();
}
void AGSPlayerController::Client_OnKill_Implementation(AGSPlayerState* KillerPlayerState,
AGSPlayerState* VictimPlayerState)
{
OnKill(KillerPlayerState, VictimPlayerState);
}
void AGSPlayerController::Client_OnDeath_Implementation(AGSPlayerState* KillerPlayerState,
AGSPlayerState* VictimPlayerState)
{
OnDeath(KillerPlayerState, VictimPlayerState);
}
void AGSPlayerController::Client_WarmupStarted_Implementation()
{
OnWarmupStarted();
}
void AGSPlayerController::Client_GameRestarted_Implementation()
{
OnGameRestarted();
}
void AGSPlayerController::ClientGameEnded_Implementation(AActor* EndGameFocus, bool bIsWinner)
{
Super::ClientGameEnded_Implementation(EndGameFocus, bIsWinner);
SetIgnoreMoveInput(true);
APawn* MyPawn = GetPawn();
if(MyPawn) // pawn may be null if we died and then the round ended
{
// disable all actions on character
MyPawn->TurnOff();
}
OnGameEnded(bIsWinner);
}
void AGSPlayerController::UnFreeze()
{
Super::UnFreeze();
ServerRestartPlayer();
}
void AGSPlayerController::AcknowledgePossession(APawn* P)
{
OnAcknowledgePossession();
Super::AcknowledgePossession(P);
}

View File

@ -0,0 +1,99 @@
// Gravity Stomp Copyright Kevin Poretti
#pragma once
// UE includes
#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "GSPlayerController.generated.h"
/**
* Player controller for a team deathmatch/free for all game mode
*/
UCLASS()
class GRAVITYSTOMPGAME_API AGSPlayerController : public APlayerController
{
GENERATED_BODY()
public:
/**
* Notify player that a game has started
*/
UFUNCTION(Reliable, Client)
void Client_GameStarted();
/**
* Notify player when the warmup period has started
*/
UFUNCTION(Reliable, Client)
void Client_WarmupStarted();
UFUNCTION(BlueprintImplementableEvent, Category="Events")
void OnWarmupStarted();
/**
* Notify a player that a game has started
*/
UFUNCTION(BlueprintImplementableEvent, Category="Events")
void OnGameStarted();
UFUNCTION(BlueprintImplementableEvent)
void OnAcknowledgePossession();
UFUNCTION(BlueprintImplementableEvent)
void OnPawnInitialized();
/**
* RPC when this player kills someone
*/
UFUNCTION(Reliable, Client)
void Client_OnKill(AGSPlayerState* KillerPlayerState, AGSPlayerState* VictimPlayerState);
/**
* Blueprint event when the OnKill RPC is called
*/
UFUNCTION(BlueprintImplementableEvent, Category="Events")
void OnKill(AGSPlayerState* KillerPlayerState, AGSPlayerState* VictimPlayerState);
/**
* RPC when a death occurs so the player can update their UI
*/
UFUNCTION(Reliable, Client)
void Client_OnDeath(AGSPlayerState* KillerPlayerState, AGSPlayerState* VictimPlayerState);
/**
* Blueprint event when the OnDeath RPC is called
*/
UFUNCTION(BlueprintImplementableEvent, Category="Events")
void OnDeath(AGSPlayerState* KillerPlayerState, AGSPlayerState* VictimPlayerState);
/**
* Blueprint event when the game ended RPC is called
*/
UFUNCTION(BlueprintImplementableEvent, Category="Events")
void OnGameEnded(bool bIsWinner);
UFUNCTION(Reliable, Client)
void Client_GameRestarted();
UFUNCTION(BlueprintImplementableEvent)
void OnGameRestarted();
// APlayerController interface
/**
* Does character cleanup when the game ends
*/
virtual void ClientGameEnded_Implementation(AActor* EndGameFocus, bool bIsWinner) override;
/**
* Request respawn from server when we are allowed
*/
virtual void UnFreeze() override;
/**
* Calls OnAcknowledgePossession so the UI can bind itself to events like health updates
*/
virtual void AcknowledgePossession(APawn* P) override;
// End APlayerController interface
};

View File

@ -0,0 +1,94 @@
// Gravity Stomp Copyright Kevin Poretti
#include "Player/GSPlayerState.h"
// GS includes
#include "Character/GSCharacter.h"
// UE includes
#include "Kismet/GameplayStatics.h"
#include "Net/UnrealNetwork.h"
AGSPlayerState::AGSPlayerState()
{
TeamNum = 0;
NumKills = 0;
NumDeaths = 0;
}
void AGSPlayerState::SetTeamNum(int32 InTeamNum)
{
TeamNum = InTeamNum;
UpdateTeamColor();
}
void AGSPlayerState::UpdateTeamColor()
{
AGSPlayerState* LocalPS = GetWorld()->GetFirstPlayerController()->GetPlayerState<AGSPlayerState>();
// if this player state belongs to us then we need to update all other characters
// rendering of team will essentially flip (all enemies are now rendered as friendly and vice versa)
if((GetPlayerController() && (GetNetMode() == ENetMode::NM_Client)) || ((GetPlayerController() == GetWorld()->GetFirstPlayerController()) && (GetNetMode() == ENetMode::NM_ListenServer)))
{
for(int32 PSIdx = 0; PSIdx < UGameplayStatics::GetNumPlayerStates(GetWorld()); ++PSIdx)
{
AGSPlayerState* CurrPS = Cast<AGSPlayerState>(UGameplayStatics::GetPlayerState(GetWorld(), PSIdx));
if(CurrPS != LocalPS) // only update team colors for remote characters
{
AGSCharacter* CurrChar = CurrPS->GetPawn<AGSCharacter>();
if(CurrChar && CurrPS && LocalPS)
{
CurrChar->ChangeTeamColor(IsPlayerFriendly(CurrPS));
}
}
}
}
else // this PS represents a remote player so we just need to switch how we are rendered to the local player
{
AGSCharacter* Char = GetPawn<AGSCharacter>();
if(Char && LocalPS)
{
Char->ChangeTeamColor(IsPlayerFriendly(LocalPS));
}
}
}
void AGSPlayerState::OnRep_TeamNum()
{
UE_LOG(LogTemp, Warning, TEXT("%s is on team %d"), *GetPlayerName(), TeamNum);
UpdateTeamColor();
}
bool AGSPlayerState::IsPlayerFriendly(const AGSPlayerState* PS) const
{
return TeamNum == PS->GetTeamNum();
}
void AGSPlayerState::Reset()
{
NumKills = 0;
NumDeaths = 0;
}
void AGSPlayerState::ClientInitialize(AController* InController)
{
Super::ClientInitialize(InController);
UpdateTeamColor();
}
void AGSPlayerState::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(AGSPlayerState, NumKills);
DOREPLIFETIME(AGSPlayerState, NumDeaths);
DOREPLIFETIME(AGSPlayerState, TeamNum);
}

View File

@ -0,0 +1,73 @@
// Gravity Stomp Copyright Kevin Poretti
#pragma once
// UE includes
#include "CoreMinimal.h"
#include "GameFramework/PlayerState.h"
#include "GSPlayerState.generated.h"
/**
* Player state for a team deathmatch/free for all game mode
*/
UCLASS()
class GRAVITYSTOMPGAME_API AGSPlayerState : public APlayerState
{
GENERATED_BODY()
public:
AGSPlayerState();
UFUNCTION(BlueprintPure)
FORCEINLINE int32 GetTeamNum() const { return TeamNum; }
void SetTeamNum(int32 InTeamNum);
UFUNCTION(BlueprintPure)
FORCEINLINE int32 GetNumKills() const { return NumKills; }
FORCEINLINE void SetNumKills(int32 InNumKills) { NumKills = InNumKills; }
FORCEINLINE void IncrementNumKills() { NumKills++; }
UFUNCTION(BlueprintPure)
FORCEINLINE int32 GetNumDeaths() const { return NumDeaths; }
FORCEINLINE void SetNumDeaths(int32 InNumDeaths) { NumDeaths = InNumDeaths; }
FORCEINLINE void IncrementNumDeaths() { NumDeaths++; }
UFUNCTION(BlueprintPure)
bool IsPlayerFriendly(const AGSPlayerState* PS) const;
// Begin APlayerState interface
/**
* Clear kills and deaths
*/
virtual void Reset() override;
/**
* Updates team color on init
*/
virtual void ClientInitialize(class AController* InController) override;
// End APlayerState interface
protected:
UPROPERTY(Replicated)
int32 NumKills;
UPROPERTY(Replicated)
int32 NumDeaths;
UPROPERTY(ReplicatedUsing=OnRep_TeamNum)
int32 TeamNum;
UFUNCTION()
void OnRep_TeamNum();
/**
* Changes how this player is rendered to a client
*/
void UpdateTeamColor();
};