Loading...
Stealth Multiplayer Game
Project Type Persoanl project
Software Used Unreal Engine 4
Languages Used C++, Blueprints
Primary Role(s) Gameplay, A.I and Multiplayer programming

What I Did



    What I did in this Project

    • Simple A.I Guard State Machine that protects the objective
    • Game logic ( Taking the objective, going to extraction point, ending the game, or get spotted and lose)
    • Multiplayer ready.
    • Some gameplay mechanics such as ( Blackhole that drains anything around, Launch pad that launches the character)


  A.I State Machine in C++
Simple A.I State Machine consist of Idle, Suspecious, Patrolling, Aletrted



Code Snippit:
// This is the .h 

class UPawnSensingComponent;

UENUM(BlueprintType)
enum class AIState : uint8
{
	Idle,
	Suspecious,
	Alerted,
	Patroling
};

UCLASS()
class FPSGAME_API AFPSAIGuard : public ACharacter
{
	GENERATED_BODY()

public:
	// Sets default values for this character's properties
	AFPSAIGuard();

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

	UFUNCTION()
	void OnPawnSeen(APawn* SeenPawn);

	UFUNCTION()
	void OnNoiseHeard(APawn* HeardInstigator, const FVector& Location, float Volume);

	UFUNCTION()
	void ResetOrientation();

	UFUNCTION()
	void OnRep_GuardState();

	UFUNCTION(BlueprintImplementableEvent, Category = "AI")
	void OnStateChanged(AIState NewState);

	void SetGuardState(AIState NewState);

	void MoveToNextPatrolPoint();


protected:

	UPROPERTY(VisibleAnywhere, Category = "Component")
	UPawnSensingComponent* SensingComponent = nullptr;

	UPROPERTY()
	FRotator OriginalRotation;

	UPROPERTY()
	FTimerHandle TimerHandle_ResetOrientation;

	UPROPERTY(ReplicatedUsing=OnRep_GuardState)
	AIState GuardState;

	///* Let the guard go on patrol */
	UPROPERTY(EditInstanceOnly, Category = "AI")
	bool bPatrol;
	///* First of two patrol points to patrol between */
	UPROPERTY(EditInstanceOnly, Category = "AI", meta = (EditCondition = "bPatrol"))
	AActor* FirstPatrolPoint;

	///* Second of two patrol points to patrol between */
	UPROPERTY(EditInstanceOnly, Category = "AI", meta = (EditCondition = "bPatrol"))
	AActor* SecondPatrolPoint = nullptr;

	//// The current point the actor is either moving to or standing at
	AActor* CurrentPatrolPoint;

public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;	
};

		


// Sets default values
AFPSAIGuard::AFPSAIGuard()
{
 	// Set this character to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

	SensingComponent = CreateDefaultSubobject(TEXT("Sensing Component"));

	SensingComponent->OnSeePawn.AddDynamic(this, &AFPSAIGuard::OnPawnSeen);
	SensingComponent->OnHearNoise.AddDynamic(this, &AFPSAIGuard::OnNoiseHeard);

	GuardState = AIState::Patroling;
}

void AFPSAIGuard::MoveToNextPatrolPoint()
{
	if (CurrentPatrolPoint == nullptr || CurrentPatrolPoint == SecondPatrolPoint )
	{
		CurrentPatrolPoint = FirstPatrolPoint;
	}
	else
	{
		CurrentPatrolPoint = SecondPatrolPoint;
	}
	UAIBlueprintHelperLibrary::SimpleMoveToActor(GetController(), CurrentPatrolPoint);
}

// Called when the game starts or when spawned
void AFPSAIGuard::BeginPlay()
{
	Super::BeginPlay();

	OriginalRotation = GetActorRotation();

	if (bPatrol)
	{
		MoveToNextPatrolPoint();
	}
}

void AFPSAIGuard::OnPawnSeen(APawn* SeenPawn)
{
	if (!SeenPawn) { return; }
	AFPSGameMode* MyGameMode = Cast(GetWorld()->GetAuthGameMode());
	if (!MyGameMode) { return; }

	MyGameMode->MissionComplete(SeenPawn, false);

	SetGuardState(AIState::Alerted);

	AController* MyController = GetController();
	if (MyController)
	{
		MyController->StopMovement();
	}
}

void AFPSAIGuard::OnNoiseHeard(APawn* HeardInstigator, const FVector& Location, float Volume)
{
	if (GuardState == AIState::Alerted)
	{
		return;
	}

	UE_LOG(LogTemp, Warning, TEXT("Hear Component:  %s"), *HeardInstigator->GetName());
	DrawDebugSphere(GetWorld(), Location, 34.0f, 12, FColor::Red, false, 10.0f);

	FVector Direction = Location - GetActorLocation();
	Direction.Normalize();
	FRotator NewLookAt = FRotationMatrix::MakeFromX(Direction).Rotator();
	NewLookAt.Pitch = 0.0f;
	NewLookAt.Roll = 0.0f;
	SetActorRotation(NewLookAt);

	GetWorldTimerManager().ClearTimer(TimerHandle_ResetOrientation);
	GetWorldTimerManager().SetTimer(TimerHandle_ResetOrientation, this, &AFPSAIGuard::ResetOrientation, 3.0f);

	SetGuardState(AIState::Suspecious);

	AController* MyController = GetController();
	if (MyController)
	{
		MyController->StopMovement();
	}
}



void AFPSAIGuard::ResetOrientation()
{
	SetActorRotation(OriginalRotation);
	SetGuardState(AIState::Patroling);

	if (bPatrol)
	{
		MoveToNextPatrolPoint();
	}
}

void AFPSAIGuard::OnRep_GuardState()
{
	OnStateChanged(GuardState);
}

void AFPSAIGuard::GetLifetimeReplicatedProps(TArray< FLifetimeProperty > & OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);

	DOREPLIFETIME(AFPSAIGuard, GuardState);
}


void AFPSAIGuard::SetGuardState(AIState NewState)
{
	if (GuardState != NewState)
	{
		GuardState = NewState;
	}
	OnRep_GuardState();
}


// Called every frame
void AFPSAIGuard::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	if (CurrentPatrolPoint)
	{
		FVector DeltaLocation = GetActorLocation() - CurrentPatrolPoint->GetActorLocation();
		float DistanceToGoal = DeltaLocation.Size();

		if (DistanceToGoal < 200)
		{
			MoveToNextPatrolPoint();
		}
	}

}
		

  Game Logic in C++ and Blueprint
This drives the game logic so that when the player gets spotted, he loses and get a simple UI on screen that tells that he lost, opposite happens when he wins.


AFPSGameMode::AFPSGameMode()
{
	// set default pawn class to our Blueprinted character
	static ConstructorHelpers::FClassFinder PlayerPawnClassFinder(TEXT("/Game/Blueprints/BP_Player"));
	DefaultPawnClass = PlayerPawnClassFinder.Class;

	// use our custom HUD class
	HUDClass = AFPSHUD::StaticClass();

	// Setting Game State
	GameStateClass = AFPSGameState::StaticClass();
}

void AFPSGameMode::MissionComplete(APawn* InstigatorPawn, bool bMissionSuccess)
{
	if (!InstigatorPawn) { return; }



	TArray FetchedActors;
	UGameplayStatics::GetAllActorsOfClass(this, Spectating, FetchedActors);

	if (FetchedActors.Num() > 0)
	{
		AActor* NewSpectateView = nullptr;
		NewSpectateView = FetchedActors[0];
		for (FConstPlayerControllerIterator it = GetWorld()->GetPlayerControllerIterator(); it; it++)
		{
			APlayerController* PC = it->Get();
			if (PC)
			{
				PC->SetViewTargetWithBlend(NewSpectateView, 0.5f, EViewTargetBlendFunction::VTBlend_Cubic);
			}
		}
	}

	AFPSGameState* GS = GetGameState();
	if (GS)
	{
		GS->MulticastOnMissionCompleted(InstigatorPawn, bMissionSuccess);
	}

	// Blueprint Implementable Event in ue4
	OnMissionCompleted(InstigatorPawn, bMissionSuccess);
}
		
In this pic we use the on mission complete event from c++ in blueprint to manage the mission complete status whether the player won or lost.

Blueprint Implementable Event

  Handling Multiplayer Mission State

We handle the mission state by using RPCs (Remote Procedure Calls) which basically uses NetMulticast which will apply the bahviour of call from the Server as well as executed on the server and all other connected Clients (Players) to make sure that to all the clinets the game has ended. and since we want this to be for all players, all this must be implemented in the GameState because in GameState as we know it holds all of our replicated informaton about current game and taht is because we can not put that in GameMode because as we know game mode only Exist on the server, and clinets do NOT actually have a copy of the game mod, that means whenever we want to replicate something we have to put it in a seperate class which is game state.


Game mode actually defines how the game is being played, it governs the game rules, scoring, what actors are allowed to exist in this game type, and who may enter the game ... etc, it is only instanced on the server and will never exist on the client coz ofc imagine a client being able to change game rules ? :D


for the PlayerState, that contains all the persistent infromation of a player and that is because the player controller also does not exist on the other clients it only exist on OUR Machine ( player machine only) and on the server, I believe that is why we need cheat protection as well ;)



So here is a code snippit on Using RPC NetMulticast in this game

Code Snippit:

Header File
// THIS IS the Header file

UCLASS()
class FPSGAME_API AFPSGameState : public AGameStateBase
{
	GENERATED_BODY()
	
	
public:
  // To ensure that an RPC call is executed on the remote machine, you can specify the Reliable keyword
	UFUNCTION(NetMulticast, Reliable)
	void MulticastOnMissionCompleted(APawn* InestigatorPawn, bool bMissionSuccess);
	
};
		


Cpp File
// CPP File
void AFPSGameState::MulticastOnMissionCompleted_Implementation(APawn* InestigatorPawn, bool bMissionSuccess)
{
	for (FConstPlayerControllerIterator it = GetWorld()->GetPlayerControllerIterator(); it; it++)
	{
		AFPSPlayerController* PlayerController = Cast(it->Get());
		if (PlayerController && PlayerController->IsLocalController())
		{
			PlayerController->OnMissionComplete(InestigatorPawn, bMissionSuccess);

			// Disable Input
			APawn* MyPawn = PlayerController->GetPawn();
			if (MyPawn)
			{
				MyPawn->DisableInput(PlayerController);
			}
		}
	}
}
		



  Gameplay Mechanics

GIF and code snippit for the blackhole and the launch pad



Blackhole

Launch Pad



Code snippit for the blackhole

// Sets default values
AFPSBlackHole::AFPSBlackHole()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

	SceneComponent = CreateDefaultSubobject(TEXT("SceneComponent"));
	RootComponent = SceneComponent;
	MeshComp = CreateDefaultSubobject(TEXT("MeshComp"));
	MeshComp->SetCollisionEnabled(ECollisionEnabled::NoCollision);

	InnerSphereComponent = CreateDefaultSubobject(TEXT("InnerSphereComp"));
	InnerSphereComponent->SetSphereRadius(100);

	// Bind to Event
	OuterSphereComponent = CreateDefaultSubobject(TEXT("OuterSphereComp"));
	OuterSphereComponent->SetSphereRadius(3000);

	// Setting Up Attachment
	MeshComp->SetupAttachment(SceneComponent);
	InnerSphereComponent->SetupAttachment(SceneComponent);
	OuterSphereComponent->SetupAttachment(SceneComponent);
}

// Called every frame
void AFPSBlackHole::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
	OuterSphereComponent->GetOverlappingComponents(Boxes);
	for (UPrimitiveComponent* Box : Boxes)
	{
		Box->AddRadialForce(GetActorLocation(), OuterSphereComponent->GetScaledSphereRadius(), -2000.0f, ERadialImpulseFalloff::RIF_Constant, true);
	}

}
		


Code snippit for the launchPad



// Sets default values
AFPSLaunchPad::AFPSLaunchPad()
{
	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

	BoxComp = CreateDefaultSubobject(TEXT("Box Component"));
	StaticMeshComp = CreateDefaultSubobject(TEXT("Mesh Component"));
	RootComponent = StaticMeshComp;
	BoxComp->SetupAttachment(RootComponent);
	BoxComp->OnComponentBeginOverlap.AddDynamic(this, &AFPSLaunchPad::LaunchOverlapedObjects);

	LaunchPitchAngle = 35.5f;
	LaunchStrength = 1500.0f;

}

void AFPSLaunchPad::LaunchOverlapedObjects(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	FRotator LaunchDirection = GetActorRotation();
	LaunchDirection.Pitch += LaunchPitchAngle;
	FVector LaunchVelocity = LaunchDirection.Vector() * LaunchStrength;
	ACharacter* MyCharacter = Cast(OtherActor);

	if (MyCharacter)
	{
		MyCharacter->LaunchCharacter(LaunchVelocity, true, true);
		//Spawn FX
		UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), ActivateLaunchPadEmitter, GetActorLocation());
	}
	else if (OtherComp && OtherComp->IsSimulatingPhysics())
	{
		OtherComp->AddImpulse(LaunchVelocity, NAME_None, true);
		UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), ActivateLaunchPadEmitter, GetActorLocation());
	}
}
		


  UI

Handle simple UI to let the player know what to do, whether they lost or won, what is the next objective.. etc



UI Handle

UI Handle



Click Here To Go To The Project On Github


Copyright © 2018 by Mazen Morgan and David Wagih