Unreal Tournament Integration¶
Here we will describe how we modified the latest Unreal Tournament to use Genvid.
Preparation¶
You will need to download the Unreal Tournament source code.
This code is available from GitHub but only to registered developers. To register, follow the steps highlighted here.
Our current code is based off of the following changelist:
commit b82f21dc6af1907201aeec58ae7d38a1bb0c9bb2
Author: Peter Knepley <[email protected]>
Date: Tue Nov 15 16:46:53 2016 -0500
Copying
//UT/Release-Next/...
to //UT/Release-Live/...
[CL 3199151 by Peter Knepley in UT-Release-Live branch]
Once you have downloaded the source code, switch to that changelist:
git checkout b82f21dc6af1907201aeec58ae7d38a1bb0c9bb2
Normally, some git-hook should call setup.bat
, but we recommend you
call it by hand for good measure. This script will download and
install all of the necessary requirements.
You should then follow the steps listed in Epic’s own documentation.
The gist of it is to run GenerateProjectFiles.bat
, open up UE4.sln,
and compile a few targets (ShaderCompileWorker and UnrealLightmass).
Then, with UnrealTournament
selected, simply Build and Debug (F5).
It does take some time, so be patient.
Make sure you have a working Unreal Tournament build before proceeding.
Installation¶
The integration requires various files which we added and some modifications to existing UE4 files to make Unreal Tournament run with Genvid.
- Copy
GenvidPlugin
underUnrealTournament/Plugins/
. - Copy
GenvidConnector.h
underUnrealTournament/Source/UnrealTournament/Public/
. - Copy
GenvidConnector.cpp
underUnrealTournament/Source/UnrealTournament/Private/
.
Details¶
This section presents the modifications to be made to several UE4 files.
UnrealTournament.Build.cs¶
At the very end of public UnrealTournament(TargetInfo Target)
add the following line:
PrivateDependencyModuleNames.Add("GenvidPlugin");
This line makes all of the GenvidPlugin public interfaces available to all of the UnrealTournament sub-project.
UTGameState¶
Some modifications are required in the AUTGameState
class, we suggest that
you surround them with preprocessor macros:
#if GENVID
// Code...
#endif //GENVID
The GENVID value will be defined in the UTGameState.h
file.
This will allow developers to quickly isolate the modifications.
Let us first describe the required changes to UTGameState.h
.
Right before:
#include "UTGameState.generated.h"
Insert the following lines:
#define GENVID 1
#if GENVID
#include "GenvidConnector.h"
#endif
Further down, at the end of the AUTGameState class definition, insert the following lines:
#if GENVID
private:
GenvidConnector Genvid;
#endif
Right after:
virtual void BeginPlay() override;
Insert the following lines:
#if GENVID
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
#endif
We will now describe the required changes to UTGameState.cpp
.
In the AUTGameState::Tick()
function, add a call to Genvid.Tick()
.
#if GENVID
Genvid.Tick(DeltaTime);
#endif
This method simply calls GenvidUpdate()
at regular intervals.
Due to the fact that UWorld::GetTimeSeconds()
judders when setting
a low TimeDilation
value, we have opted to use UWorld::GetRealTimeSeconds()
instead.
void GenvidConnector::Tick(float DeltaSeconds)
{
// Note: GetTimeSeconds() is the world time, and judders when dilation is really slow, so we use GetRealTimeSeconds().
float CurTime = GWorld->GetRealTimeSeconds();
if (CurTime >= GenvidNextTriggerTime)
{
GenvidUpdate(GWorld);
GenvidNextTriggerTime = CurTime + GenvidUpdateDelay;
}
}
The GenvidUpdate()
method is where the bulk of the game data is
both gathered and sent:
void GenvidConnector::GenvidUpdate(UWorld *World)
{
GenvidTimecode tc = Genvid_GetCurrentTimecode();
// Submit game data.
if(bSendGameData)
{
GenvidStatus gs;
FGenvidData_Locations Locations;
CollectGameData(World, Locations, GenvidPlayersInfo);
std::string LocationsJSON = FGenvidUtils::ConvertFStructToJSONStdString(Locations);
gs = Genvid_SubmitGameData(tc, sStreamID_Locations.c_str(), LocationsJSON.data(), (int)LocationsJSON.size());
if(GENVID_FAILED(gs))
{
UE_LOG(LogGenvid, Error, TEXT("Genvid_SubmitGameData(%s) failed: %s (%d)."), UTF8_TO_TCHAR(sStreamID_Locations.c_str()), UTF8_TO_TCHAR(Genvid_StatusToString(gs)), (int)gs);
}
std::string PlayersInfoJSON = FGenvidUtils::ConvertFStructToJSONStdString(GenvidPlayersInfo);
gs = Genvid_SubmitGameData(tc, sStreamID_PlayerInfo.c_str(), PlayersInfoJSON.data(), (int)PlayersInfoJSON.size());
if(GENVID_FAILED(gs))
{
UE_LOG(LogGenvid, Error, TEXT("Genvid_SubmitGameData(%s) failed: %s (%d)."), UTF8_TO_TCHAR(sStreamID_PlayerInfo.c_str()), UTF8_TO_TCHAR(Genvid_StatusToString(gs)), (int)gs);
}
// Submit notifications for popularity
FGenvidData_PlayersCheerInfo GenvidCheersInfo;
GenvidPlayersInfo.CollectCheerInfo(&GenvidCheersInfo);
std::string CheersInfoJSON = FGenvidUtils::ConvertFStructToJSONStdString(GenvidCheersInfo);
gs = Genvid_SubmitNotification(sStreamID_CheersInfo.c_str(), CheersInfoJSON.data(), (int)CheersInfoJSON.size());
//Modifying the game speed with the KV
float resultSpeed = 0.0f;
GenvidStatus valueSpeed = Genvid_GetParameterFloat("genvid.kv", "ut4/gamespeed", &resultSpeed);
if (resultSpeed > 0.0f)
{
resultSpeed = FMath::Clamp(resultSpeed, 0.0001f, 20.0f); // Same as in CheatManager::Slomo().
GWorld->GetWorldSettings()->TimeDilation = resultSpeed;
}
else
{
UE_LOG(LogGenvid, Warning, TEXT("Invalid game speed."));
}
}
// Handle MapReduce results.
GenvidStatus gs = Genvid_CheckForEvents();
if(GENVID_FAILED(gs) && gs != GenvidStatus_ConnectionTimeout)
{
UE_LOG(LogGenvid, Error, TEXT("Genvid_CheckForEvents() failed: %s (%d)."), UTF8_TO_TCHAR(Genvid_StatusToString(gs)), (int)gs);
}
// Handle of player popularity.
FString MostPopular;
float HighestPopularity = 0.0f;
for (auto Iter = GenvidPlayersInfo.Players.CreateIterator(); Iter; ++Iter)
{
FGenvidData_PlayerInfo& PlayerInfo = *Iter;
PlayerInfo.Popularity -= GenvidUpdateDelay; // Lose 1 unit every second.
if (PlayerInfo.Popularity < 0.f)
{
PlayerInfo.Popularity = 0.f;
}
if (PlayerInfo.Popularity > HighestPopularity)
{
MostPopular = PlayerInfo.Name;
HighestPopularity = PlayerInfo.Popularity;
}
}
// Update new lead.
AUTPlayerController* CurrentCameraController = GetCurrentCameraController(World);
if (CurrentCameraController)
{
if(HighestPopularity > 0.0f)
{
if(GenvidCurrentLead != MostPopular)
{
AUTPlayerState* LeadPlayerState = GetPlayerState(World, *MostPopular);
if(LeadPlayerState)
{
CurrentCameraController->ViewPlayerNum(LeadPlayerState->SpectatingID);
GenvidCurrentLead = MostPopular;
UE_LOG(LogGenvid, Log, TEXT("New Lead is '%s'."), *GenvidCurrentLead);
}
else
{
CurrentCameraController->EnableAutoCam();
GenvidCurrentLead.Empty();
UE_LOG(LogGenvid, Log, TEXT("Failed to set lead to '%s'."), *MostPopular);
}
}
}
else
{
if(!GenvidCurrentLead.IsEmpty())
{
CurrentCameraController->EnableAutoCam();
GenvidCurrentLead.Empty();
UE_LOG(LogGenvid, Log, TEXT("Cleared lead; switching to auto-cam."));
}
}
}
// Report camera changes.
if (CurrentCameraController)
{
if (GenvidLastSpectatedPlayerState != CurrentCameraController->LastSpectatedPlayerState)
{
// Camera changed target.
GenvidLastSpectatedPlayerState = CurrentCameraController->LastSpectatedPlayerState;
FGenvidData_CurrentCamera CurrentCamera;
if(GenvidLastSpectatedPlayerState)
{
CurrentCamera.Name = GenvidLastSpectatedPlayerState->PlayerName;
}
std::string CurrentCameraJSON = FGenvidUtils::ConvertFStructToJSONStdString(CurrentCamera);
/*GenvidStatus*/ gs = Genvid_SubmitGameData(tc, sStreamID_CurrentCamera.c_str(), CurrentCameraJSON.data(), (int)CurrentCameraJSON.size());
if(GENVID_FAILED(gs))
{
UE_LOG(LogGenvid, Error, TEXT("Genvid_SubmitGameData(%s) failed: %s (%d)."), UTF8_TO_TCHAR(sStreamID_CurrentCamera.c_str()), UTF8_TO_TCHAR(Genvid_StatusToString(gs)), (int)gs);
}
UE_LOG(LogGenvid, Log, TEXT("Camera changed to look at '%s'."), *CurrentCamera.Name);
}
}
if (GenvidConfirmedKills.Kills.Num() > 0)
{
// Confirm player kills.
std::string ConfirmedKillsJSON = FGenvidUtils::ConvertFStructToJSONStdString(GenvidConfirmedKills);
gs = Genvid_SubmitAnnotation(tc, sStreamID_ConfirmedKills.c_str(), ConfirmedKillsJSON.data(), (int)ConfirmedKillsJSON.size());
if (GENVID_FAILED(gs))
{
UE_LOG(LogGenvid, Error, TEXT("Genvid_SubmitGameData(%s) failed: %s (%d)."), UTF8_TO_TCHAR(sStreamID_ConfirmedKills.c_str()), UTF8_TO_TCHAR(Genvid_StatusToString(gs)), (int)gs);
}
GenvidConfirmedKills.Reset();
}
}
This method gathers player and camera information with
CollectGameData()
, converts them to JSON format using the
FGenvidUtils::ConvertFStructToJSONStdString()
utility routine, and
then submits it on appropriate game data streams so it can reach the
website.
Note
Using UE4 JSON serialization routines is a suboptimal decision which we took mainly due to simplicity arguments of this tutorial. We recommend developers use a much more compact binary representation in order to save as much bandwidth as possible. The extra cost incurred on the web browser side should be worth the extra effort.
The function Genvid_CheckForEvents()
is then called, which itself
will possibly invoke calls to AUTGameState::GenvidSubscriptionCallback()
.
This will handle results coming from the event channels.
Following that, we have code which fades player popularity over time, and detects the lead player. The camera will then focus on that lead player, or revert back to auto-cam mode if popularity is depleted.
When the camera starts following a new player (detected by
using the GenvidLastSpectatedPlayerState
member), a separate message
is sent to the website.
After that, we report back confirming the players killed remotely.
The GenvidSubscriptionCallback()
method gets called
automatically whenever event data is available:
void GenvidConnector::GenvidSubscriptionCallback(const struct GenvidEventSummary *summary)
{
UE_LOG(LogGenvid, VeryVerbose, TEXT("GenvidSubscriptionCallback(%s[%d])"), UTF8_TO_TCHAR(summary->id), summary->numResults);
if (summary->id == sReduceID_KillPlayer)
{
for (int i = 0; i < summary->numResults; ++i)
{
const auto & result = summary->results[i];
FString PlayerName = UTF8_TO_TCHAR(result.key.fields[1]); // ["kill_player", "<name>"]
APawn* PlayerPawn = nullptr;
AUTPlayerState* PlayerState = GetPlayerState(GWorld, PlayerName, &PlayerPawn);
if (PlayerPawn)
{
UE_LOG(LogGenvid, Log, TEXT("Killing '%s'."), *PlayerName);
AActor* Actor = PlayerPawn;
float DamageAmount = MAX_FLT; //INFINITY
FVector HitLoc = Actor->GetActorLocation();
FVector HitDir = FVector(0.0f, 0.0f, 1.0f);
FHitResult Hit = FHitResult(Actor, nullptr, HitLoc, HitDir);
FPointDamageEvent DamageEvent = FPointDamageEvent(DamageAmount, Hit, -HitDir, UDamageType::StaticClass());
Actor->TakeDamage(DamageAmount, DamageEvent, nullptr, nullptr);
GenvidConfirmedKills.Kills.Add(PlayerName);
}
else
{
UE_LOG(LogGenvid, Warning, TEXT("'Failed to kill '%s'."), *PlayerName);
}
}
}
else if (summary->id == sReduceID_Cheer)
{
for (size_t r = 0; r < summary->numResults; ++r)
{
const auto & result = summary->results[r];
FString playerName = UTF8_TO_TCHAR(result.key.fields[1]); // The key is ["cheer", <playername>]
for (size_t v = 0; v < result.numValues; ++v)
{
const auto & value = result.values[v];
if (value.reduce == GenvidReduceOp_Count)
{
FGenvidData_PlayerInfo* playerInfo = GenvidPlayersInfo.Players.FindByKey(playerName);
if (playerInfo != nullptr)
{
UE_LOG(LogGenvid, Log, TEXT("Cheering '%s'."), *playerName);
playerInfo->Popularity += (5 * value.value); // 5s per cheer.
if(playerInfo->Popularity > 60.0f)
{
playerInfo->Popularity = 60.0f;
}
}
else
{
UE_LOG(LogGenvid, Warning, TEXT("Invalid player cheered: '%s'"), *playerName);
continue;
}
}
}
}
}
else
{
UE_LOG(LogGenvid, Warning, TEXT("Received unknown subject '%s'."), UTF8_TO_TCHAR(summary->id));
return;
}
}
// Callback routine invoked by Genvid_CheckForEvents() to handle commands.
void GenvidConnector::GenvidSubscriptionCommandCallback(const GenvidCommandResult* InCmd)
{
//std::string id(cmd->id);
size_t Pos;
std::string CmdFull = InCmd->value;
Pos = CmdFull.find(' ');
std::string Cmd = CmdFull.substr(0, Pos);
Pos = CmdFull.find_first_not_of(' ', Pos);
std::string Args;
if(Pos != std::string::npos)
{
Args = CmdFull.substr(Pos);
}
if(Cmd == "changeCamera")
{
FString CameraName = UTF8_TO_TCHAR(Args.c_str());
if(CameraName.IsEmpty())
{
UE_LOG(LogGenvid, Log, TEXT("Changing camera back to auto-cam."));
AUTPlayerController* CurrentCameraController = GetCurrentCameraController(GWorld);
if(CurrentCameraController)
{
CurrentCameraController->EnableAutoCam();
}
return;
}
else
{
// Search for a player of the specified name.
APawn* PlayerPawn = nullptr;
AUTPlayerState* PlayerState = GetPlayerState(GWorld, CameraName, &PlayerPawn);
if (PlayerState)
{
AUTPlayerController* CurrentCameraController = GetCurrentCameraController(GWorld);
UE_LOG(LogGenvid, Log, TEXT("Changing camera to player '%s'."), *CameraName);
if(CurrentCameraController)
{
CurrentCameraController->ViewPlayerNum(PlayerState->SpectatingID);
}
return;
}
// Search for spectating camera of the specified name..
int32 CamCount = 0;
for (FActorIterator It(GWorld); It; ++It)
{
AUTSpectatorCamera* Cam = Cast<AUTSpectatorCamera>(*It);
if (Cam)
{
if (Cam->CamLocationName == CameraName)
{
UE_LOG(LogGenvid, Log, TEXT("Changing camera to '%s'."), *CameraName);
AUTPlayerController* Controller = GetCurrentCameraController(GWorld);
if (Controller)
{
Controller->ViewCamera(CamCount);
return;
}
else
{
UE_LOG(LogGenvid, Warning, TEXT("Game cheat 'ChangeCamera' couldn't find a proper controller."));
return;
}
}
++CamCount;
}
}
UE_LOG(LogGenvid, Warning, TEXT("Couldn't find camera named '%s'."), *CameraName);
return;
}
}
UE_LOG(LogGenvid, Warning, TEXT("Invalid command: '%s'."), UTF8_TO_TCHAR(InCmd->value));
}
It simply interprets the returning events, and triggers proper game behavior, such as changing camera to follow a newly-defined lead, setting a new game speed (to allow for slow-motion), or even killing a specific player (and reporting back that it was performed).
In the AUTGameState::BeginPlay()
function, add a call to Genvid.BeginPlay()
in
order to proceed with the initialization process for the GenvidPlugin.
#if GENVID
Genvid.BeginPlay(PrimaryActorTick);
#endif
In this call the following will be done:
- We create all the streams required for the game (video stream and data stream).
- We subscribe to Event channels that we want to receive.
- We start the video auto capture using a utility routine from
FGenvidUtils
. - We report that
BeginPlay()
was called. - We set various variables used by the
Tick()
function, which we also enable.
void GenvidConnector::BeginPlay(FActorTickFunction &PrimaryActorTick)
{
GAreScreenMessagesEnabled = false;
GScreenMessagesRestoreState = false;
GenvidStatus gs;
// Genvid_Initialize() is handled by the GenvidPlugin.
// Create data streams.
gs = Genvid_CreateStream(sStreamID_Audio.c_str());
if (GENVID_FAILED(gs))
{
UE_LOG(LogGenvid, Error, TEXT("Genvid_CreateStream(%s) failed: %s (%d)."), UTF8_TO_TCHAR(sStreamID_Audio.c_str()), UTF8_TO_TCHAR(Genvid_StatusToString(gs)), (int)gs);
}
gs = Genvid_CreateStream(sStreamID_Video.c_str());
if (GENVID_FAILED(gs))
{
UE_LOG(LogGenvid, Error, TEXT("Genvid_CreateStream(%s) failed: %s (%d)."), UTF8_TO_TCHAR(sStreamID_Video.c_str()), UTF8_TO_TCHAR(Genvid_StatusToString(gs)), (int)gs);
}
gs = Genvid_CreateStream(sStreamID_GameState.c_str());
if (GENVID_FAILED(gs))
{
UE_LOG(LogGenvid, Error, TEXT("Genvid_CreateStream(%s) failed: %s (%d)."), UTF8_TO_TCHAR(sStreamID_GameState.c_str()), UTF8_TO_TCHAR(Genvid_StatusToString(gs)), (int)gs);
}
gs = Genvid_CreateStream(sStreamID_CurrentCamera.c_str());
if (GENVID_FAILED(gs))
{
UE_LOG(LogGenvid, Error, TEXT("Genvid_CreateStream(%s) failed: %s (%d)."), UTF8_TO_TCHAR(sStreamID_CurrentCamera.c_str()), UTF8_TO_TCHAR(Genvid_StatusToString(gs)), (int)gs);
}
gs = Genvid_CreateStream(sStreamID_ConfirmedKills.c_str());
if (GENVID_FAILED(gs))
{
UE_LOG(LogGenvid, Error, TEXT("Genvid_CreateStream(%s) failed: %s (%d)."), UTF8_TO_TCHAR(sStreamID_ConfirmedKills.c_str()), UTF8_TO_TCHAR(Genvid_StatusToString(gs)), (int)gs);
}
gs = Genvid_CreateStream(sStreamID_TeamInfo.c_str());
if (GENVID_FAILED(gs))
{
UE_LOG(LogGenvid, Error, TEXT("Genvid_CreateStream(%s) failed: %s (%d)."), UTF8_TO_TCHAR(sStreamID_TeamInfo.c_str()), UTF8_TO_TCHAR(Genvid_StatusToString(gs)), (int)gs);
}
gs = Genvid_CreateStream(sStreamID_MatchInfo.c_str());
if (GENVID_FAILED(gs))
{
UE_LOG(LogGenvid, Error, TEXT("Genvid_CreateStream(%s) failed: %s (%d)."), UTF8_TO_TCHAR(sStreamID_MatchInfo.c_str()), UTF8_TO_TCHAR(Genvid_StatusToString(gs)), (int)gs);
}
gs = Genvid_CreateStream(sStreamID_PlayerInfo.c_str());
if (GENVID_FAILED(gs))
{
UE_LOG(LogGenvid, Error, TEXT("Genvid_CreateStream(%s) failed: %s (%d)."), UTF8_TO_TCHAR(sStreamID_PlayerInfo.c_str()), UTF8_TO_TCHAR(Genvid_StatusToString(gs)), (int)gs);
}
gs = Genvid_CreateStream(sStreamID_Locations.c_str());
if (GENVID_FAILED(gs))
{
UE_LOG(LogGenvid, Error, TEXT("Genvid_CreateStream(%s) failed: %s (%d)."), UTF8_TO_TCHAR(sStreamID_Locations.c_str()), UTF8_TO_TCHAR(Genvid_StatusToString(gs)), (int)gs);
}
auto gameEventCallback = [](const GenvidEventSummary *summary, void *userData)
{
auto This = reinterpret_cast<GenvidConnector*>(userData);
This->GenvidSubscriptionCallback(summary);
};
sGameEventCallback = (GenvidEventSummaryCallback)gameEventCallback;
// Define event subscriptions
gs = Genvid_Subscribe(sReduceID_Cheer.c_str(), sGameEventCallback, this);
if (GENVID_FAILED(gs))
{
UE_LOG(LogGenvid, Error, TEXT("Genvid_Subscribe(%s) failed: %s (%d)."), UTF8_TO_TCHAR(sReduceID_Cheer.c_str()), UTF8_TO_TCHAR(Genvid_StatusToString(gs)), (int)gs);
}
gs = Genvid_Subscribe(sReduceID_KillPlayer.c_str(), sGameEventCallback, this);
if (GENVID_FAILED(gs))
{
UE_LOG(LogGenvid, Error, TEXT("Genvid_Subscribe(%s) failed: %s (%d)."), UTF8_TO_TCHAR(sReduceID_KillPlayer.c_str()), UTF8_TO_TCHAR(Genvid_StatusToString(gs)), (int)gs);
}
// Define command subscriptions.
auto commandCallback = [](const GenvidCommandResult* result, void* userData)
{
auto This = reinterpret_cast<GenvidConnector*>(userData);
This->GenvidSubscriptionCommandCallback(result);
};
sGameCommandCallback = (GenvidCommandCallback)commandCallback;
gs = Genvid_SubscribeCommand(sCommandID_Game.c_str(), sGameCommandCallback, this);
if (GENVID_FAILED(gs))
{
UE_LOG(LogGenvid, Error, TEXT("Genvid_SubscribeCommand(%s) failed: %s (%d)."), UTF8_TO_TCHAR(sCommandID_Game.c_str()), UTF8_TO_TCHAR(Genvid_StatusToString(gs)), (int)gs);
}
// Activate auto-capture for the current window.
if (!FGenvidUtils::StartAutoCapture(sStreamID_Audio.c_str(), sStreamID_Video.c_str(), GEngine->GameViewport->GetWindow().Get()))
{
UE_LOG(LogGenvid, Error, TEXT("FGenvidUtils::StartAutoCapture(%s, %s) failed."), UTF8_TO_TCHAR(sStreamID_Audio.c_str()), UTF8_TO_TCHAR(sStreamID_Video.c_str()));
}
// Inform website of BeginPlay().
FGenvidData_GameState GameState("Playing");
std::string GameStateJSON = FGenvidUtils::ConvertFStructToJSONStdString(GameState);
gs = Genvid_SubmitGameData(-1, sStreamID_GameState.c_str(), GameStateJSON.c_str(), (int)GameStateJSON.size());
if (GENVID_FAILED(gs))
{
UE_LOG(LogGenvid, Error, TEXT("Genvid_SubmitGameData(%s) failed: %s (%d)."), UTF8_TO_TCHAR(sStreamID_GameState.c_str()), UTF8_TO_TCHAR(Genvid_StatusToString(gs)), (int)gs);
}
// Start sending game data.
bSendGameData = true;
// Internal state bookkeeping.
GenvidLastSpectatedPlayerState = nullptr;
sGameEventCallback = nullptr;
GenvidPlayersInfo.Reset();
GenvidTeamsInfo.Reset();
GenvidConfirmedKills.Reset();
GenvidCurrentLead.Empty();
// Internal state bookkeeping.
GenvidLastSpectatedPlayerState = nullptr;
GenvidUpdateDelay = 1.0f / 30.0f;
GenvidNextTriggerTime = 0.0f; // Trigger right away.
PrimaryActorTick.SetTickFunctionEnable(true);
}
After the AUTGameState::BeginPlay()
function, add the following function:
#if GENVID
void AUTGameState::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
Super::EndPlay(EndPlayReason);
Genvid.EndPlay(EndPlayReason);
}
#endif
In the EndPlay()
function of the GenvidConnector the opposite of the initialization
process is performed:
void GenvidConnector::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
GenvidStatus gs;
// Inform website of EndPlay().
FGenvidData_GameState GameState("");
std::string GameStateJSON = FGenvidUtils::ConvertFStructToJSONStdString(GameState);
gs = Genvid_SubmitGameData(-1, sStreamID_GameState.c_str(), GameStateJSON.c_str(), (int)GameStateJSON.size());
if(GENVID_FAILED(gs))
{
UE_LOG(LogGenvid, Error, TEXT("Genvid_SubmitGameData(%s) failed: %s (%d)."), UTF8_TO_TCHAR(sStreamID_GameState.c_str()), UTF8_TO_TCHAR(Genvid_StatusToString(gs)), (int)gs);
}
// Disable auto-capture.
FGenvidUtils::StopAutoCapture(sStreamID_Audio.c_str(), sStreamID_Video.c_str());
gs = Genvid_UnsubscribeCommand(sCommandID_Game.c_str(), sGameCommandCallback, this);
if(GENVID_FAILED(gs))
{
UE_LOG(LogGenvid, Error, TEXT("Genvid_UnsubscribeCommand(%s) failed: %s (%d)."), UTF8_TO_TCHAR(sCommandID_Game.c_str()), UTF8_TO_TCHAR(Genvid_StatusToString(gs)), (int)gs);
}
gs = Genvid_Unsubscribe(sReduceID_KillPlayer.c_str(), sGameEventCallback, this);
if(GENVID_FAILED(gs))
{
UE_LOG(LogGenvid, Error, TEXT("Genvid_Unsubscribe(%s) failed: %s (%d)."), UTF8_TO_TCHAR(sReduceID_KillPlayer.c_str()), UTF8_TO_TCHAR(Genvid_StatusToString(gs)), (int)gs);
}
gs = Genvid_Unsubscribe(sReduceID_Cheer.c_str(), sGameEventCallback, this);
if(GENVID_FAILED(gs))
{
UE_LOG(LogGenvid, Error, TEXT("Genvid_Unsubscribe(%s) failed: %s (%d)."), UTF8_TO_TCHAR(sReduceID_Cheer.c_str()), UTF8_TO_TCHAR(Genvid_StatusToString(gs)), (int)gs);
}
// Destroy streams.
gs = Genvid_DestroyStream(sStreamID_Locations.c_str());
if(GENVID_FAILED(gs))
{
UE_LOG(LogGenvid, Error, TEXT("Genvid_DestroyStream(%s) failed: %s (%d)."), UTF8_TO_TCHAR(sStreamID_Locations.c_str()), UTF8_TO_TCHAR(Genvid_StatusToString(gs)), (int)gs);
}
gs = Genvid_DestroyStream(sStreamID_PlayerInfo.c_str());
if(GENVID_FAILED(gs))
{
UE_LOG(LogGenvid, Error, TEXT("Genvid_DestroyStream(%s) failed: %s (%d)."), UTF8_TO_TCHAR(sStreamID_PlayerInfo.c_str()), UTF8_TO_TCHAR(Genvid_StatusToString(gs)), (int)gs);
}
gs = Genvid_DestroyStream(sStreamID_MatchInfo.c_str());
if(GENVID_FAILED(gs))
{
UE_LOG(LogGenvid, Error, TEXT("Genvid_DestroyStream(%s) failed: %s (%d)."), UTF8_TO_TCHAR(sStreamID_MatchInfo.c_str()), UTF8_TO_TCHAR(Genvid_StatusToString(gs)), (int)gs);
}
gs = Genvid_DestroyStream(sStreamID_TeamInfo.c_str());
if(GENVID_FAILED(gs))
{
UE_LOG(LogGenvid, Error, TEXT("Genvid_DestroyStream(%s) failed: %s (%d)."), UTF8_TO_TCHAR(sStreamID_TeamInfo.c_str()), UTF8_TO_TCHAR(Genvid_StatusToString(gs)), (int)gs);
}
gs = Genvid_DestroyStream(sStreamID_ConfirmedKills.c_str());
if(GENVID_FAILED(gs))
{
UE_LOG(LogGenvid, Error, TEXT("Genvid_DestroyStream(%s) failed: %s (%d)."), UTF8_TO_TCHAR(sStreamID_ConfirmedKills.c_str()), UTF8_TO_TCHAR(Genvid_StatusToString(gs)), (int)gs);
}
gs = Genvid_DestroyStream(sStreamID_CurrentCamera.c_str());
if(GENVID_FAILED(gs))
{
UE_LOG(LogGenvid, Error, TEXT("Genvid_DestroyStream(%s) failed: %s (%d)."), UTF8_TO_TCHAR(sStreamID_CurrentCamera.c_str()), UTF8_TO_TCHAR(Genvid_StatusToString(gs)), (int)gs);
}
gs = Genvid_DestroyStream(sStreamID_GameState.c_str());
if(GENVID_FAILED(gs))
{
UE_LOG(LogGenvid, Error, TEXT("Genvid_DestroyStream(%s) failed: %s (%d)."), UTF8_TO_TCHAR(sStreamID_GameState.c_str()), UTF8_TO_TCHAR(Genvid_StatusToString(gs)), (int)gs);
}
gs = Genvid_DestroyStream(sStreamID_Video.c_str());
if(GENVID_FAILED(gs))
{
UE_LOG(LogGenvid, Error, TEXT("Genvid_DestroyStream(%s) failed: %s (%d)."), UTF8_TO_TCHAR(sStreamID_Video.c_str()), UTF8_TO_TCHAR(Genvid_StatusToString(gs)), (int)gs);
}
gs = Genvid_DestroyStream(sStreamID_Audio.c_str());
if(GENVID_FAILED(gs))
{
UE_LOG(LogGenvid, Error, TEXT("Genvid_DestroyStream(%s) failed: %s (%d)."), UTF8_TO_TCHAR(sStreamID_Audio.c_str()), UTF8_TO_TCHAR(Genvid_StatusToString(gs)), (int)gs);
}
// Genvid_Terminate() is handled by the GenvidPlugin.
// Internal state bookkeeping.
GenvidLastSpectatedPlayerState = nullptr;
sGameEventCallback = nullptr;
GenvidPlayersInfo.Reset();
GenvidTeamsInfo.Reset();
GenvidConfirmedKills.Reset();
GenvidCurrentLead.Empty();
}
In the AUTGameState::HandleMatchHasStarted()
function, add a call to Genvid.HandleMatchHasStarted()
.
#if GENVID
Genvid.HandleMatchHasStarted(GetWorld(), PlayerArray, Teams);
#endif
In the HandleMatchHasStarted()
method, we retrieve the player list
for later use. We then send an event to indicate that the match has
started. We also collect and send team information to the website.
Finally, we set a boolean to actually start sending game events.
void GenvidConnector::HandleMatchHasStarted(UWorld* InWorld, TArray<class APlayerState*>& PlayerArray, TArray<AUTTeamInfo*>& Teams)
{
if (!bSendGameData) {
UE_LOG(LogGenvid, Warning, TEXT("HandleMatchHasStarted: Streams not created. Error will happen."));
}
GenvidStatus gs;
GenvidTimecode tc = Genvid_GetCurrentTimecode();
// Collect match information.
if(CollectMatchInfo(InWorld, GenvidMatchInfo))
{
std::string MatchInfoJSON = FGenvidUtils::ConvertFStructToJSONStdString(GenvidMatchInfo);
gs = Genvid_SubmitGameData(tc, sStreamID_MatchInfo.c_str(), MatchInfoJSON.c_str(), (int)MatchInfoJSON.size());
if (GENVID_FAILED(gs))
{
UE_LOG(LogGenvid, Error, TEXT("Genvid_SubmitGameData(%s) failed: %s (%d)."), UTF8_TO_TCHAR(sStreamID_MatchInfo.c_str()), UTF8_TO_TCHAR(Genvid_StatusToString(gs)), (int)gs);
}
}
// Add all players (could not find any UT4 event for these, and they are all available here).
GenvidPlayersInfo.Reset();
for (int i = 0; i < PlayerArray.Num(); ++i)
{
APlayerState* Player = PlayerArray[i];
if (!Player->bIsSpectator)
{
GenvidPlayersInfo.Players.Emplace(Player->PlayerName);
}
}
// Publish match start.
FGenvidData_GameState GameState("MatchStarted");
std::string GameStateJSON = FGenvidUtils::ConvertFStructToJSONStdString(GameState);
gs = Genvid_SubmitGameData(-1, sStreamID_GameState.c_str(), GameStateJSON.c_str(), (int)GameStateJSON.size());
if (GENVID_FAILED(gs))
{
UE_LOG(LogGenvid, Error, TEXT("Genvid_SubmitGameData(%s) failed: %s (%d)."), UTF8_TO_TCHAR(sStreamID_GameState.c_str()), UTF8_TO_TCHAR(Genvid_StatusToString(gs)), (int)gs);
}
// Collect and send new team info.
GenvidTeamsInfo.Reset();
for (int i = 0; i < Teams.Num(); ++i)
{
AUTTeamInfo* Team = Teams[i];
GenvidTeamsInfo.Teams.Emplace(Team->TeamIndex, Team->TeamName, Team->TeamColor);
}
std::string TeamInfoJSON = FGenvidUtils::ConvertFStructToJSONStdString(GenvidTeamsInfo);
gs = Genvid_SubmitGameData(tc, sStreamID_TeamInfo.c_str(), TeamInfoJSON.data(), (int)TeamInfoJSON.size());
if (GENVID_FAILED(gs))
{
UE_LOG(LogGenvid, Error, TEXT("Genvid_SubmitGameData(%s) failed: %s (%d)."), UTF8_TO_TCHAR(sStreamID_TeamInfo.c_str()), UTF8_TO_TCHAR(Genvid_StatusToString(gs)), (int)gs);
}
}
In the AUTGameState::HandleMatchHasEnded()
function, add a call to Genvid.HandleMatchHasEnded()
.
#if GENVID
Genvid.HandleMatchHasEnded();
#endif
Inversely, when HandleMatchHasEnded()
is called, we stop sending
game data on every frame, and report the end of the match to the
website.
void GenvidConnector::HandleMatchHasEnded()
{
// Stop sending game data.
bSendGameData = false;
// Publish match end.
FGenvidData_GameState GameState("MatchEnded");
std::string GameStateJSON = FGenvidUtils::ConvertFStructToJSONStdString(GameState);
GenvidStatus gs = Genvid_SubmitGameData(-1, sStreamID_GameState.c_str(), GameStateJSON.c_str(), (int)GameStateJSON.size());
if (GENVID_FAILED(gs))
{
UE_LOG(LogGenvid, Error, TEXT("Genvid_SubmitGameData(%s) failed: %s (%d)."), UTF8_TO_TCHAR(sStreamID_GameState.c_str()), UTF8_TO_TCHAR(Genvid_StatusToString(gs)), (int)gs);
}
}
// Collects various match information when it starts.
bool GenvidConnector::CollectMatchInfo(UWorld* InWorld, FGenvidData_MatchInfo& OutMatchInfo)
{
OutMatchInfo.MapName = UUTGameplayStatics::GetLevelName(InWorld);
for (FActorIterator It(InWorld); It; ++It)
{
AUTSpectatorCamera* Cam = Cast<AUTSpectatorCamera>(*It);
if (Cam)
{
OutMatchInfo.SpectatorCameras.Add(Cam->CamLocationName);
}
}
return true;
}
The remaining routines CollectGameData()
, GetCurrentCameraController
,
and GetPlayerState()
are simple utility routines used to gather
specific information in the UWorld
instance.