From 1fc53e9e62bc40b589d966a8b946247207b06714 Mon Sep 17 00:00:00 2001 From: Kevin Poretti Date: Sun, 8 Jan 2023 17:33:25 -0500 Subject: [PATCH] Start of game mode --- GravityStomp/Config/DefaultGame.ini | 7 +- .../Content/Core/BP_GSDefaultGameMode.uasset | 2 +- GravityStomp/Content/Maps/Debug/Test.umap | 2 +- .../Character/GSCharacter.cpp | 8 +- .../GravityStompGame/Character/GSCharacter.h | 4 +- .../GSCharacterMovementComponent.cpp | 8 +- .../Character/GSCharacterMovementComponent.h | 2 +- .../GameModes/GSGameModeBase.cpp | 304 +++++++++++++++++- .../GameModes/GSGameModeBase.h | 117 ++++++- .../GameModes/GSGameState.cpp | 96 ++++++ .../GravityStompGame/GameModes/GSGameState.h | 73 +++++ .../GravityStompGame/GravityStompGame.h | 2 +- .../Player/GSPlayerController.cpp | 68 ++++ .../Player/GSPlayerController.h | 99 ++++++ .../GravityStompGame/Player/GSPlayerState.cpp | 94 ++++++ .../GravityStompGame/Player/GSPlayerState.h | 73 +++++ 16 files changed, 941 insertions(+), 18 deletions(-) create mode 100644 GravityStomp/Source/GravityStompGame/GameModes/GSGameState.cpp create mode 100644 GravityStomp/Source/GravityStompGame/GameModes/GSGameState.h create mode 100644 GravityStomp/Source/GravityStompGame/Player/GSPlayerController.cpp create mode 100644 GravityStomp/Source/GravityStompGame/Player/GSPlayerController.h create mode 100644 GravityStomp/Source/GravityStompGame/Player/GSPlayerState.cpp create mode 100644 GravityStomp/Source/GravityStompGame/Player/GSPlayerState.h diff --git a/GravityStomp/Config/DefaultGame.ini b/GravityStomp/Config/DefaultGame.ini index a20bb21..3c0f67a 100644 --- a/GravityStomp/Config/DefaultGame.ini +++ b/GravityStomp/Config/DefaultGame.ini @@ -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") + diff --git a/GravityStomp/Content/Core/BP_GSDefaultGameMode.uasset b/GravityStomp/Content/Core/BP_GSDefaultGameMode.uasset index 2a4c7b3..75d5582 100644 --- a/GravityStomp/Content/Core/BP_GSDefaultGameMode.uasset +++ b/GravityStomp/Content/Core/BP_GSDefaultGameMode.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a3f4c6e480bef100246dfb738cff34bfe17287948972e6241908a91da306372f +oid sha256:71ac319d3c498105d343f0cc842f0e76c9d33533ebb6798a4ac660b7f60d1707 size 19808 diff --git a/GravityStomp/Content/Maps/Debug/Test.umap b/GravityStomp/Content/Maps/Debug/Test.umap index 166ae61..39bef71 100644 --- a/GravityStomp/Content/Maps/Debug/Test.umap +++ b/GravityStomp/Content/Maps/Debug/Test.umap @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:075af1c013f7de313b3bb00d98a658d8c6762ee49da224cee009312166abfb78 +oid sha256:5ae0fbaebbd839061f4e416b0b3a9e84edd8345f95237591ea754c4c1183b464 size 47476 diff --git a/GravityStomp/Source/GravityStompGame/Character/GSCharacter.cpp b/GravityStomp/Source/GravityStompGame/Character/GSCharacter.cpp index 3b57f64..5bc0705 100644 --- a/GravityStomp/Source/GravityStompGame/Character/GSCharacter.cpp +++ b/GravityStomp/Source/GravityStompGame/Character/GSCharacter.cpp @@ -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(); diff --git a/GravityStomp/Source/GravityStompGame/Character/GSCharacter.h b/GravityStomp/Source/GravityStompGame/Character/GSCharacter.h index 9fe92cc..6f2c261 100644 --- a/GravityStomp/Source/GravityStompGame/Character/GSCharacter.h +++ b/GravityStomp/Source/GravityStompGame/Character/GSCharacter.h @@ -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 */ diff --git a/GravityStomp/Source/GravityStompGame/Character/GSCharacterMovementComponent.cpp b/GravityStomp/Source/GravityStompGame/Character/GSCharacterMovementComponent.cpp index b9d4ae2..9fff52c 100644 --- a/GravityStomp/Source/GravityStompGame/Character/GSCharacterMovementComponent.cpp +++ b/GravityStomp/Source/GravityStompGame/Character/GSCharacterMovementComponent.cpp @@ -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 { diff --git a/GravityStomp/Source/GravityStompGame/Character/GSCharacterMovementComponent.h b/GravityStomp/Source/GravityStompGame/Character/GSCharacterMovementComponent.h index 89b910c..485c56a 100644 --- a/GravityStomp/Source/GravityStompGame/Character/GSCharacterMovementComponent.h +++ b/GravityStomp/Source/GravityStompGame/Character/GSCharacterMovementComponent.h @@ -1,4 +1,4 @@ -// Fill out your copyright notice in the Description page of Project Settings. +// Gravity Stomp Copyright Kevin Poretti #pragma once diff --git a/GravityStomp/Source/GravityStompGame/GameModes/GSGameModeBase.cpp b/GravityStomp/Source/GravityStompGame/GameModes/GSGameModeBase.cpp index 76e8ddf..30ba24c 100644 --- a/GravityStomp/Source/GravityStompGame/GameModes/GSGameModeBase.cpp +++ b/GravityStomp/Source/GravityStompGame/GameModes/GSGameModeBase.cpp @@ -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(); + 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(); + 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()); + + AGSPlayerController* PC = Cast(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(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() : nullptr; + AGSPlayerState* VictimPS = Victim ? Victim->GetPlayerState() : 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 NumTeamPlayers; + AGSGameState* GS = GetGameState(); + NumTeamPlayers.AddZeroed(GS->GetNumTeams()); + + check(NumTeamPlayers.Num() > 0); + + for(int32 PlayerIdx = 0; PlayerIdx < GS->PlayerArray.Num(); ++PlayerIdx) + { + AGSPlayerState* CurrPS = Cast(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() : nullptr; + AGSPlayerState* VictimPS = Victim ? Victim->GetPlayerState() : 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(*It); + if(PC) + { + PC->Client_OnDeath(KillerPS, VictimPS); + } + } + + // give killer's team a point and check if score limit is reached + AGSGameState* GS = GetGameState(); + check(GS); + int32 NewScore = GS->GiveTeamPoint(KillerPS->GetTeamNum()); + if(NewScore >= ScoreLimit) + { + FinishMatch(); + } + } +} + + +void AGSGameModeBase::DefaultTimer() +{ + AGSGameState* GS = GetGameState(); + 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(); + 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()->GetTeamNum() == WinningTeamIdx); + } + + // turn off all pawns + for (APawn* Pawn : TActorRange(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(); + 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(*It); + if(PC) + { + PC->Client_WarmupStarted(); + } + } +} + + +void AGSGameModeBase::HandleMatchHasStarted() +{ + Super::HandleMatchHasStarted(); + + // setup in progress match + AGSGameState* GS = GetGameState(); + if(GS && GS->GetRemainingTime() <= 0) + { + GS->SetRemainingTime(RoundTime); + } + + // notify players match has started + for(FConstPlayerControllerIterator It = GetWorld()->GetPlayerControllerIterator(); It; ++It) + { + AGSPlayerController* PC = Cast(*It); + if(PC) + { + PC->Client_GameStarted(); + } + } +} + +void AGSGameModeBase::RestartGame() +{ + // notify players game has restarted + for(FConstPlayerControllerIterator It = GetWorld()->GetPlayerControllerIterator(); It; ++It) + { + AGSPlayerController* PC = Cast(*It); + if(PC) + { + PC->Client_GameRestarted(); + } + } + + Super::RestartGame(); +} \ No newline at end of file diff --git a/GravityStomp/Source/GravityStompGame/GameModes/GSGameModeBase.h b/GravityStomp/Source/GravityStompGame/GameModes/GSGameModeBase.h index 1196a78..b449cf3 100644 --- a/GravityStomp/Source/GravityStompGame/GameModes/GSGameModeBase.h +++ b/GravityStomp/Source/GravityStompGame/GameModes/GSGameModeBase.h @@ -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; }; diff --git a/GravityStomp/Source/GravityStompGame/GameModes/GSGameState.cpp b/GravityStomp/Source/GravityStompGame/GameModes/GSGameState.cpp new file mode 100644 index 0000000..8b29d71 --- /dev/null +++ b/GravityStomp/Source/GravityStompGame/GameModes/GSGameState.cpp @@ -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); +} \ No newline at end of file diff --git a/GravityStomp/Source/GravityStompGame/GameModes/GSGameState.h b/GravityStomp/Source/GravityStompGame/GameModes/GSGameState.h new file mode 100644 index 0000000..48c575c --- /dev/null +++ b/GravityStomp/Source/GravityStompGame/GameModes/GSGameState.h @@ -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&, 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 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(); +}; diff --git a/GravityStomp/Source/GravityStompGame/GravityStompGame.h b/GravityStomp/Source/GravityStompGame/GravityStompGame.h index ddbf2e2..d405ddc 100644 --- a/GravityStomp/Source/GravityStompGame/GravityStompGame.h +++ b/GravityStomp/Source/GravityStompGame/GravityStompGame.h @@ -1,4 +1,4 @@ -// Copyright Epic Games, Inc. All Rights Reserved. +// Gravity Stomp Copyright Kevin Poretti #pragma once diff --git a/GravityStomp/Source/GravityStompGame/Player/GSPlayerController.cpp b/GravityStomp/Source/GravityStompGame/Player/GSPlayerController.cpp new file mode 100644 index 0000000..aff6915 --- /dev/null +++ b/GravityStomp/Source/GravityStompGame/Player/GSPlayerController.cpp @@ -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); +} \ No newline at end of file diff --git a/GravityStomp/Source/GravityStompGame/Player/GSPlayerController.h b/GravityStomp/Source/GravityStompGame/Player/GSPlayerController.h new file mode 100644 index 0000000..753fe83 --- /dev/null +++ b/GravityStomp/Source/GravityStompGame/Player/GSPlayerController.h @@ -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 +}; diff --git a/GravityStomp/Source/GravityStompGame/Player/GSPlayerState.cpp b/GravityStomp/Source/GravityStompGame/Player/GSPlayerState.cpp new file mode 100644 index 0000000..d393cfd --- /dev/null +++ b/GravityStomp/Source/GravityStompGame/Player/GSPlayerState.cpp @@ -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(); + // 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(UGameplayStatics::GetPlayerState(GetWorld(), PSIdx)); + if(CurrPS != LocalPS) // only update team colors for remote characters + { + AGSCharacter* CurrChar = CurrPS->GetPawn(); + 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(); + 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& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + DOREPLIFETIME(AGSPlayerState, NumKills); + DOREPLIFETIME(AGSPlayerState, NumDeaths); + DOREPLIFETIME(AGSPlayerState, TeamNum); +} \ No newline at end of file diff --git a/GravityStomp/Source/GravityStompGame/Player/GSPlayerState.h b/GravityStomp/Source/GravityStompGame/Player/GSPlayerState.h new file mode 100644 index 0000000..6dbd1ce --- /dev/null +++ b/GravityStomp/Source/GravityStompGame/Player/GSPlayerState.h @@ -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(); +};