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

What I Did





Game Gif

Game pic

  A.I Behaviour Tree
A.I Behaviour Tree that runs some sequences depending on the A.I status whether he is trying to find a cover, wants to attack the player, or taking cover to regenerate its Health.

It Uses EQS (Environmental Query System) to find cover and to find potential players to attack.

Here are some pics of the behaviour tree, EQS, Tasks.

A task I implemented that is used in the Behaviour Tree to attack the target. text_to_describe_your_image

A task I implemented that is used in the Behaviour Tree to make the A.I Regenerates its HP. text_to_describe_your_image

An EQS to Find the nearest player EQS to Find Nearest Player



And Here is the complete Behaviour Tree. So basically in this picture right below the root, we have a Selected that select the sensed player according to an implemented service,
Then it checks since we know the selector checks everything if one succeded it goes with it then start over again, so the selecters checks first node and we have a Decorator that tells whether the Enemy A.I
Can Regen Health or not, if it can it will start the sequence where we Run the EQS then move to targetDestination (which is taking cover) then heal and wait 1 second
if we have a player to attack then we go with the mid node, which has a selector to decide whether to start searching for the player or begin Movement Sequence
after that we wait a random Amout of time then start the Task of attacking the target, if all that fails we are gonna start the sequence of finding nearest player and moving to the nearest player
this one actually is a bit hacky because the A.I knows the player location but it acts like "ugh I am walking this way I wonder who is there" and it defeinilty knows who is there xD.

Behaviour Tree




  Weapon System
As I said above, Here are some pics of the weapon system and a code snippit from the weapons base class

Weapon Properties ! this is a weapon using Line tracing ( Ray cast ) to damage the Enemy, I have also created a grenade launcher. Weapon Properties

Here is a code snippit for the Weapon Class. you can also see the Multiplayer Functionality Added in this class for ex ServerFire()


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


	MeshComp = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("Mesh Component"));
	RootComponent = MeshComp;

	MuzzleSocketName = "MuzzleSocket";
	TracerTargetName = "Target";

	BaseDamage = 25.0f;
	BulletSpread = 10.0f;
	HeadShotDamageMultiplier = 4.0f;

	RateOfFire = 600.0f;

	// Network
	SetReplicates(true);

	NetUpdateFrequency = 66.0f;
	MinNetUpdateFrequency = 33.0f;
}


void ASWeapon::BeginPlay()
{
	Super::BeginPlay();

	TimeBetweenShots = 60 / RateOfFire;
}


void ASWeapon::Fire()
{
	if (Role < ROLE_Authority)
	{
		ServerFire();

	}

	AActor* MyOwner = GetOwner();
	if (MyOwner)
	{
		FVector EyeLocation;
		FRotator EyeRotation;
		MyOwner->GetActorEyesViewPoint(EyeLocation, EyeRotation);

		FVector HitDirection = EyeRotation.Vector();
		float HalfRad = FMath::DegreesToRadians(BulletSpread);
		HitDirection = FMath::VRandCone(HitDirection, HalfRad, HalfRad);


		FVector TraceEnd = EyeLocation + (HitDirection * 10000);
		FVector TracerEndPoint = TraceEnd;


		EPhysicalSurface SurfaceType = SurfaceType_Default;

		FCollisionQueryParams QueryParams;
		QueryParams.AddIgnoredActor(MyOwner);
		QueryParams.AddIgnoredActor(this);
		QueryParams.bTraceComplex = true;
		QueryParams.bReturnPhysicalMaterial = true;

		FHitResult Hit;
		bool bHitSuccess = GetWorld()->LineTraceSingleByChannel(Hit, EyeLocation, TraceEnd, COLLISION_WEAPON, QueryParams);
		if (bHitSuccess)
		{
			AActor* HitActor = Hit.GetActor();
			SurfaceType = UPhysicalMaterial::DetermineSurfaceType(Hit.PhysMaterial.Get());

			float ActualDamage = BaseDamage;
			if (SurfaceType == SURFACE_FLESH_VULNRABLE)
			{
				ActualDamage *= HeadShotDamageMultiplier;
			}

			UGameplayStatics::ApplyPointDamage(HitActor, ActualDamage, HitDirection, Hit, MyOwner->GetInstigatorController(), MyOwner, DamageType);

			PlayImpactEffect(SurfaceType, Hit.ImpactPoint);

			TracerEndPoint = Hit.ImpactPoint;
		}
		if (DebugWeaponDrawing > 0)
		{
			DrawDebugLine(GetWorld(), EyeLocation, TraceEnd, FColor::Red, false, 2.0f);
		}
		PlayFireEffects(TracerEndPoint);

		if (Role == ROLE_Authority)
		{
			HitScanTrace.TraceTo = TracerEndPoint;
			HitScanTrace.SurfaceType = SurfaceType;
		}

		LastFireTime = GetWorld()->TimeSeconds;
	}

}


void ASWeapon::PlayImpactEffect(EPhysicalSurface SurfaceType, FVector ImpactPoint)
{
	UParticleSystem* SelectedEffect = nullptr;

	switch (SurfaceType)
	{
	case SURFACE_FLESH_DEFAULT:
		SelectedEffect = FleshImpactEffect;
		break;
	case SURFACE_FLESH_VULNRABLE:
		SelectedEffect = FleshImpactEffect;
		break;
	default:
		SelectedEffect = DefaultImpactEffect;
		break;
	}

	if (SelectedEffect)
	{
		FVector MuzzleLocation = MeshComp->GetSocketLocation(MuzzleSocketName);

		FVector ShotDirection = ImpactPoint - MuzzleLocation;
		ShotDirection.Normalize();

		UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), SelectedEffect, ImpactPoint, ShotDirection.Rotation());
	}
}

void ASWeapon::OnRep_HitScanTrace()
{
	// PlayCosmiticEffects
	PlayFireEffects(HitScanTrace.TraceTo);
	PlayImpactEffect(HitScanTrace.SurfaceType, HitScanTrace.TraceTo);
}

void ASWeapon::ServerFire_Implementation()
{
	Fire();
}

bool ASWeapon::ServerFire_Validate()
{
	return true;
}

void ASWeapon::StartFire()
{
	float FirstDelay = FMath::Max(LastFireTime + TimeBetweenShots - GetWorld()->TimeSeconds, 0.0f);

	GetWorldTimerManager().SetTimer(TimeHandle_TimeBetweenShots, this, &ASWeapon::Fire, TimeBetweenShots, true, FirstDelay);
}

void ASWeapon::StopFire()
{
	GetWorldTimerManager().ClearTimer(TimeHandle_TimeBetweenShots);
}

void ASWeapon::PlayFireEffects(FVector TracerEndPoint)
{
	if (MuzzleEffect)
	{
		UGameplayStatics::SpawnEmitterAttached(MuzzleEffect, MeshComp, MuzzleSocketName);
	}

	FVector MuzzleLocation = MeshComp->GetSocketLocation(MuzzleSocketName);
	if (TraceEffect)
	{
		auto TracerParticleComp = UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), TraceEffect, MuzzleLocation);
		if (TracerParticleComp)
		{
			TracerParticleComp->SetVectorParameter(TracerTargetName, TracerEndPoint);
		}
	}

	APawn* MyOwner = Cast<APawn>(GetOwner());

	if (MyOwner)
	{
		APlayerController* PC = Cast<APlayerController>(MyOwner->GetController());
		if (PC)
		{
			PC->ClientPlayCameraShake(CameraShakeClass);
		}
	}

}


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

	DOREPLIFETIME_CONDITION(ASWeapon, HitScanTrace, COND_SkipOwner);
}
		

  Pickup Abilities
A health Pickup ability and movememnt speed booster ability implemented in C++ and Blueprints.

pick up abilities
This was my first interaction with Abilities Pickup in Unreal engine 4, they are so simple and very important to have in any game to boost player experience
			// Sets default values
ASPowerupActor::ASPowerupActor()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = false;

	TotalNumberOfTicks = 0;

	PowerupInterval = 0.0f;

	SetReplicates(true);
}


void ASPowerupActor::OnTickPowerup(AActor* ActivateForActor)
{
	TicksProcessed++;
	OnPowerupTicked(ActivateForActor);

	if (TicksProcessed >= TotalNumberOfTicks)
	{
		OnExpierd();

		bIsPowerupActive = false;
		OnRep_PowerupActive();

		// Delete Timer !
		GetWorldTimerManager().ClearTimer(TimerHandle_PowerupTick);
	}
}

void ASPowerupActor::OnRep_PowerupActive()
{
	OnPowerStateChanged(bIsPowerupActive);
}


void ASPowerupActor::ActivatePowerup(AActor* ActivateForActor)
{
	OnActivated(ActivateForActor);
	
	bIsPowerupActive = true;
	OnRep_PowerupActive();

	if (PowerupInterval > 0.0f)
	{
		/*
			using timer delegete so we can be able to Bind a UFUNCTION,
			by doing this we can pass the function with parameters 
		*/
		FTimerDelegate TimerDelegate_OnTickPowerup;
		TimerDelegate_OnTickPowerup.BindUFunction(this, FName("OnTickPowerup"), ActivateForActor);

		GetWorldTimerManager().SetTimer(TimerHandle_PowerupTick, TimerDelegate_OnTickPowerup, PowerupInterval, true);
	}
	else
	{
		OnTickPowerup(ActivateForActor);
	}
}

void ASPowerupActor::GetLifetimeReplicatedProps(TArray< FLifetimeProperty > & OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
	DOREPLIFETIME(ASPowerupActor, bIsPowerupActive);
}
		


Using Blueprint Implementable Event!
Blueprint Implementable Event


  Spawning and Counting Score
Spawning Enemy A.I According to Wave System that I will discuss right after this section. Also the spawning uses Target Actors and EQS to spawn with a random enemy type to spawn
whether it is a TrackerBot (Ill discuss the bot after) or Human Enemy A.I

Spawning Enemy

Counting and Adding player score and adding it, the score is consitant for all the players
Spawning Enemy




  Custom physical Surface type

Surface Type

That is how damage is calculated using Phys material (Custom physical surface type from the previous pic),
we basically check what is the phy material coming back from the populated "Hit" when we make the line tracing (ray casting), then we decide what damage to apply, whether we deal more damage , play certain effect, sound or add damage multipier... etc

Head Physical Surface

Body Physical Surface


  Simple A.I Tracker bot that exploades when near the player

Tracker bot Gif

In this picture we determine the volume by the rolling speed of the tracker bot, in other words the volume gets higher when the tracker bot rolls faster
Rolling

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

	ExplosionDamage = 60;
	ExplosionRadius = 350	;
	MovementForce = 1000.0f;
	Accurecy = 50.0f;
	bUseVelocityChange = true;
	SelfDestructInterval = 0.5f;

	MeshComp = CreateDefaultSubobject(TEXT("Mesh Component"));
	MeshComp->SetCanEverAffectNavigation(false);
	MeshComp->SetSimulatePhysics(true);
	RootComponent = MeshComp;

	HealthComp = CreateDefaultSubobject(TEXT("Health Component"));
	HealthComp->OnHealthChanged.AddDynamic(this, &ASTrackerBot::HandleTakeDamage);

	SphereComp = CreateDefaultSubobject(TEXT("Sphere Component"));
	SphereComp->SetupAttachment(RootComponent);
	SphereComp->SetSphereRadius(ExplosionRadius);
	SphereComp->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
	SphereComp->SetCollisionResponseToAllChannels(ECR_Ignore);
	SphereComp->SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap);

	SetReplicates(true);

}

// Called when the game starts or when spawned
void ASTrackerBot::BeginPlay()
{
	Super::BeginPlay();
	if (Role == ROLE_Authority)
	{
		NextPathPoint = GetNextPathPoint();
	}
}


FVector ASTrackerBot::GetNextPathPoint()
{
	AActor* BestTarget = nullptr;
	float NearestTargetDistance = FLT_MAX;

	for (FConstPawnIterator It = GetWorld()->GetPawnIterator(); It; ++It)
	{
		APawn* TestPawn = It->Get();
		if (TestPawn == nullptr || USHealthComponent::IsFriendly(TestPawn, this))
		{
			continue;
		}

		USHealthComponent* TestPawnHealthComp = Cast(TestPawn->GetComponentByClass(USHealthComponent::StaticClass()));

		if (TestPawnHealthComp && TestPawnHealthComp->GetHealth() > 0)
		{
			float Distance = (TestPawn->GetActorLocation() - GetActorLocation()).Size();
			if (NearestTargetDistance > Distance)
			{
				BestTarget = TestPawn;
				NearestTargetDistance = Distance;
			}
		}
	}


	// To use UNavigationSystemV1 this we must add NavigationSystem in the CoopGame.Build.cs module.

	if (BestTarget)
	{
		auto NavPath = UNavigationSystemV1::FindPathToActorSynchronously(this, GetActorLocation(), BestTarget, 50.0f);
		
		GetWorldTimerManager().ClearTimer(TimerHandle_RefreshPath);
		GetWorldTimerManager().SetTimer(TimerHandle_RefreshPath, this, &ASTrackerBot::RefreshPath, 5.0f, false);

		if (!NavPath) { return FVector::ZeroVector; }

		if (NavPath->PathPoints.Num() > 1)
		{
			// Return next point in the path
			return NavPath->PathPoints[1];
		}
	}

	// if it fails return actor location!
	return GetActorLocation();
}


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

	MoveToPlayer();

}
		



  Wave State System


This is a wave system made in c++, for example We spawn 2 enemies at first then when they die, we check the WaveState and confirm that they are dead then we wait
for specified time that can be changed in the Editor and then we spawn the 2nd wave the 2nd wave would be twice as big as the previous wave... and so on

		enum class EWaveState : uint8;

// Custom Event
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FOnActorKilled, AActor*, VictimActor, AActor*, KillerActor, AController*, KillerController);

/**
 * 
 */
UCLASS()
class COOPGAME_API ASGameMode : public AGameModeBase
{
	GENERATED_BODY()
	
public:
	ASGameMode();

	/** Transitions to calls BeginPlay on actors. */
	virtual void StartPlay() override;

	virtual void Tick(float DeltaSeconds) override;

	UPROPERTY(BlueprintAssignable, Category = "SGameMode")
	FOnActorKilled OnActorKilled;

protected:

	UPROPERTY(EditDefaultsOnly, Category = "SGameMode")
	float TimeBetweenWaves;

	FTimerHandle TimerHandle_SpawnBot;

	FTimerHandle TimerHandle_NextWaveStart;

	int32 NumberOfBotsToSpawn;

	int32 WaveCount;
	
protected:

	UFUNCTION(BlueprintImplementableEvent, Category = "SGameMode")
	void SpawnNewBot();

	// Start Spawning Bot and start a timer
	void StartWave();

	// Stop spawning bots and reset Timer
	void EndWave();

	// Set Timer for next wave
	void PrepareForNextWave();

	void CheckWaveState();

	void CheckAnyPlayerAlive();

	void GameOver();

	void SetWaveState(EWaveState NewState);
	
	UFUNCTION()
	void SpawnBotTimerElapsed();

	void RestartDeadPlayer();

ASGameMode::ASGameMode()
{
	GameStateClass = ASGameState::StaticClass();
	PlayerStateClass = ASPlayerState::StaticClass();

	TimeBetweenWaves = 5;
	PrimaryActorTick.bCanEverTick = true;
	PrimaryActorTick.TickInterval = 0.5;
}


void ASGameMode::StartPlay()
{
	Super::StartPlay();

	SetWaveState(EWaveState::StartGame);

	PrepareForNextWave();
}

void ASGameMode::Tick(float DeltaSeconds)
{
	Super::Tick(DeltaSeconds);

	CheckWaveState();
	CheckAnyPlayerAlive();
}

void ASGameMode::StartWave()
{
	WaveCount++;

	NumberOfBotsToSpawn = 2 * WaveCount;

	GetWorldTimerManager().SetTimer(TimerHandle_SpawnBot, this, &ASGameMode::SpawnBotTimerElapsed, 2.0, true, 0.0f);

	SetWaveState(EWaveState::WaveInProgress);
}


void ASGameMode::SpawnBotTimerElapsed()
{
	SpawnNewBot();

	NumberOfBotsToSpawn--;

	if (NumberOfBotsToSpawn <= 0)
	{
		EndWave();
	}
}


void ASGameMode::RestartDeadPlayer()
{
	for (FConstPlayerControllerIterator It = GetWorld()->GetPlayerControllerIterator(); It; ++It)
	{
		APlayerController* PC = It->Get();
		if (PC && PC->GetPawn() == nullptr)
		{
			RestartPlayer(PC);
		}
	}
}


void ASGameMode::EndWave()
{
	GetWorldTimerManager().ClearTimer(TimerHandle_SpawnBot);
	SetWaveState(EWaveState::WaitingToComplete);
}


void ASGameMode::CheckWaveState()
{

	bool bIsPreparingForWave = GetWorldTimerManager().IsTimerActive(TimerHandle_NextWaveStart);

	if (NumberOfBotsToSpawn > 0 || bIsPreparingForWave)
	{
		return;
	}

	bool bIsAnybotAlive = false;

	for (FConstPawnIterator It = GetWorld()->GetPawnIterator(); It; ++It)
	{
		APawn* TestPawn = It->Get();
		if (TestPawn == nullptr || TestPawn->IsPlayerControlled())
		{
			continue;
		}
		
		USHealthComponent* HealthComp = Cast(TestPawn->GetComponentByClass(USHealthComponent::StaticClass()));

		if (HealthComp && HealthComp->GetHealth() > 0)
		{
			bIsAnybotAlive = true;
			break;
		}
	}

	if (!bIsAnybotAlive)
	{
		SetWaveState(EWaveState::WaveComplete);
		PrepareForNextWave();
	}

}


void ASGameMode::CheckAnyPlayerAlive()
{
	for (FConstPlayerControllerIterator It = GetWorld()->GetPlayerControllerIterator(); It; ++It)
	{
		APlayerController* PC = It->Get();
		if (PC && PC->GetPawn())
		{
			APawn* MyPawn = PC->GetPawn();
			USHealthComponent* HealthComp = Cast(MyPawn->GetComponentByClass(USHealthComponent::StaticClass()));
			if (ensure(HealthComp) && HealthComp->GetHealth() > 0.0f)
			{
				return;
			}
		}
	}

	// All players are dead. 

	GameOver();
}

void ASGameMode::GameOver()
{
	EndWave();

	SetWaveState(EWaveState::GameOver);

	// @TODO: Finish up the match and present game over to the player along with game stats.

	UE_LOG(LogTemp, Warning, TEXT("Game Over: Owari da"));
}

void ASGameMode::SetWaveState(EWaveState NewState)
{
	ASGameState* GM = GetGameState();
	if (ensureAlways(GM))
	{
		GM->SetWaveState(NewState);
	}
}

void ASGameMode::PrepareForNextWave()
{
	GetWorldTimerManager().SetTimer(TimerHandle_NextWaveStart, this, &ASGameMode::StartWave, TimeBetweenWaves, false);
	RestartDeadPlayer();
	SetWaveState(EWaveState::WaitingForStart);
}
		




Click Here To Go To The Project On Github


Copyright © 2018 by Mazen Morgan and David Wagih