I wanted to give a try in this tool to learn more about it and also to prove that I can adapt to different toolsets.
I used a simple prototype polygon asset pack to blockout the level.
It took me 3 weeks to build this prototype, during this time I learned how to script world events and characters abilities using Angelsrcipt + Online Networking.
Character Class
import Character.COOPHealthComponent;
import Weapons.COOPWeapon;
import UI.COOPHUDWidget;
event void FOnDisarm();
class ACoopCharacter : ACharacter
{
default CapsuleComponent.SetCollisionResponseToChannel(ECollisionChannel::ECC_GameTraceChannel1, ECollisionResponse::ECR_Ignore);
//Components
UPROPERTY(DefaultComponent, BlueprintReadOnly, Category = "Components")
USpringArmComponent SpringArm;
default SpringArm.bUsePawnControlRotation = true;
UPROPERTY(DefaultComponent, BlueprintReadOnly, Category = "Components", Attach = SpringArm)
UCameraComponent CharacterCamera;
UPROPERTY(DefaultComponent, Category = "Components")
UInputComponent PlayerInputComponent;
UPROPERTY(DefaultComponent, Category = "Components")
UCOOPHealthComponent HealthComponent;
default HealthComponent.SetActive(false);
UPROPERTY(Replicated)
FOnDisarm DisarmSignature;
//Character
UPROPERTY()
float WalkingSpeed = 430.0f;
UPROPERTY()
float RunningSpeed = 550.0f;
UPROPERTY(Replicated)
float NewSpeed;
UPROPERTY(Replicated, Category = "Character")
bool bIsCrouch;
UPROPERTY(ReplicatedUsing=OnRep_PlayerIsDead, Category = "Character")
bool bIsDead;
UPROPERTY(Replicated, Category = "Character")
bool bIsZooming;
UPROPERTY(Replicated, Category = "Character")
bool bIsFiring;
UPROPERTY(Replicated, Category = "Character")
float CharacterDeathLifeSpan = 3.0f;
UPROPERTY(Replicated, Category = "Character")
bool bIsReloading;
//Weapon
UPROPERTY(Replicated, AdvancedDisplay, Category = "Weapons")
TSubclassOf MyWeaponClass;
UPROPERTY(Replicated, Category = "Weapons")
ACoopWeapon MyWeaponReference;
UPROPERTY(Category = "Weapons")
TSubclassOf FireCamShake;
UPROPERTY(Category = "Weapons")
bool bShakeCamera;
UPROPERTY(Replicated)
float AimYaw;
UPROPERTY(Replicated)
float AimPitch;
UPROPERTY(Replicated)
bool bCanDisarm = false;
UPROPERTY()
TSubclassOf CrosshairRef;
UPROPERTY()
TSubclassOf HudRef;
UPROPERTY(DefaultComponent)
UWidgetComponent PlayerHUD;
TSubclassOf MyDamageType;
default bUseControllerRotationYaw = false;
default SetReplicates(true);
default bReplicateMovement = true;
UFUNCTION(BlueprintOverride)
void BeginPlay()
{
BindInput();
HealthComponent.OnDealDamageSignature.AddUFunction(this, n"DamageImpactAnimation");
if(LocalRole == ENetRole::ROLE_Authority)
{
bIsDead = false;
bIsReloading = false;
HealthComponent.OnDeadSignature.AddUFunction(this, n"OnPlayerDeath");
SpawnWeapon(MyWeaponClass);
NewSpeed = WalkingSpeed;
}
}
UFUNCTION(BlueprintOverride)
void Tick(float DeltaSeconds)
{
CharacterMovement.MaxWalkSpeed = FMath::FInterpTo(CharacterMovement.MaxWalkSpeed, NewSpeed, DeltaSeconds, 5.0f);
if(LocalRole == ENetRole::ROLE_Authority)
{
UpdateYaw();
}
}
UFUNCTION(Server)
void UpdateYaw()
{
AimYaw = FMath::ClampAngle(FRotator(FRotator(GetControlRotation() - GetActorRotation()).Normalized).Yaw, -90.0f, 90.0f);
AimPitch = FMath::ClampAngle(FRotator(FRotator(GetControlRotation() - GetActorRotation()).Normalized).Pitch, -90.0f, 90.0f);
}
UFUNCTION()
void BindInput()
{
if(PlayerInputComponent != nullptr)
{
PlayerInputComponent.BindAxis(n"GoForward", FInputAxisHandlerDynamicSignature(this, n"MoveForward"));
PlayerInputComponent.BindAxis(n"GoRight", FInputAxisHandlerDynamicSignature(this, n"MoveRight"));
PlayerInputComponent.BindAxis(n"MousePitch", FInputAxisHandlerDynamicSignature(this, n"LookUp"));
PlayerInputComponent.BindAxis(n"MouseYaw", FInputAxisHandlerDynamicSignature(this, n"Turn"));
PlayerInputComponent.BindAction(n"Sprint", EInputEvent::IE_Pressed, FInputActionHandlerDynamicSignature(this, n"BeginSprint"));
PlayerInputComponent.BindAction(n"Sprint", EInputEvent::IE_Released, FInputActionHandlerDynamicSignature(this, n"EndSprint"));
PlayerInputComponent.BindAction(n"Crouch", EInputEvent::IE_Pressed, FInputActionHandlerDynamicSignature(this, n"BeginCrouch"));
PlayerInputComponent.BindAction(n"Crouch", EInputEvent::IE_Released, FInputActionHandlerDynamicSignature(this, n"EndCrouch"));
PlayerInputComponent.BindAction(n"Jump", EInputEvent::IE_Released, FInputActionHandlerDynamicSignature(this, n"BeginJump"));
PlayerInputComponent.BindAction(n"Disarm", EInputEvent::IE_Pressed, FInputActionHandlerDynamicSignature(this, n"Disarm"));
PlayerInputComponent.BindAction(n"Reload", EInputEvent::IE_Pressed, FInputActionHandlerDynamicSignature(this, n"StartReloadWeapon"));
PlayerInputComponent.BindAction(n"Fire", EInputEvent::IE_Pressed, FInputActionHandlerDynamicSignature(this, n"StartTrigger"));
PlayerInputComponent.BindAction(n"Fire", EInputEvent::IE_Released, FInputActionHandlerDynamicSignature(this, n"EndTrigger"));
}
}
/*
* Input Binds
*/
UFUNCTION()
void MoveForward(float AxisValue)
{
AddMovementInput(GetActorForwardVector() * AxisValue);
GetMovementComponent().NavAgentProps.bCanCrouch = true;
}
UFUNCTION()
void MoveRight(float Value)
{
AddMovementInput(GetActorRightVector() * Value);
}
UFUNCTION()
void LookUp(float Value)
{
AddControllerPitchInput(-Value);
}
UFUNCTION()
void Turn(float Value)
{
AddControllerYawInput(Value);
}
UFUNCTION()
void BeginCrouch(FKey Key)
{
Crouch();
if(LocalRole < ENetRole::ROLE_Authority)
{
ServerCrouch(true);
}
}
UFUNCTION()
void EndCrouch(FKey Key)
{
UnCrouch();
if(LocalRole < ENetRole::ROLE_Authority)
{
ServerCrouch(false);
}
}
UFUNCTION()
void BeginSprint(FKey Key)
{
if(bIsCrouched == false)
{
ServerSprint(RunningSpeed);
}
else
{
ServerSprint(WalkingSpeed);
}
}
UFUNCTION()
void EndSprint(FKey Key)
{
if(LocalRole < ENetRole::ROLE_Authority)
{
ServerSprint(WalkingSpeed);
}
}
UFUNCTION()
void BeginJump(FKey Key)
{
if(!bWasJumping)
{
Jump();
}
}
UFUNCTION()
void StartTrigger(FKey Key)
{
MyWeaponReference.StartFire();
return;
}
UFUNCTION(BlueprintEvent)
void EndTrigger(FKey Key)
{
MyWeaponReference.EndFire();
bIsFiring = false;
return;
}
UFUNCTION(BlueprintEvent)
void StartReloadWeapon(FKey Key)
{
if(LocalRole < ENetRole::ROLE_Authority)
{
ServerReload();
}
if(!bIsReloading && !MyWeaponReference.CanReload())
{
MyWeaponReference.bIsCurrentReloading = true;
bIsReloading = true;
}
}
UFUNCTION()
void Disarm(FKey Key)
{
if(LocalRole < ENetRole::ROLE_Authority)
{
ServerDisarm();
return;
}
if(bCanDisarm)
{
P();
DisarmSignature.Broadcast();
}
}
UFUNCTION(BlueprintEvent)
void OnPlayerDeath()
{
if(!bIsDead)
{
//DEBUG
if(CVar_DebugWeaponDrawing.GetInt() > 0)
{
Print("I'm dead", 1.0f, FLinearColor::LucBlue);
}
if(LocalRole == ENetRole::ROLE_Authority)
{
bIsDead = true;
}
//Detach Weapon
if(MyWeaponReference != nullptr)
{
MyWeaponReference.ServerDetachWeapon();
MyWeaponReference.FireSignature.Clear();
MyWeaponReference = nullptr;
}
//Disable Input + Destroy Actor
DetachFromControllerPendingDestroy();
SetLifeSpan(CharacterDeathLifeSpan);
}
return;
}
/*
* Weapon
*/
UFUNCTION()
void SpawnWeapon(TSubclassOf WeaponClass)
{
if(WeaponClass.IsValid())
{
AActor MySpawnedWeapon = SpawnActor(WeaponClass, FVector::ZeroVector, FRotator::ZeroRotator);
MySpawnedWeapon.AttachToComponent(Mesh, n"WeaponSocket", EAttachmentRule::SnapToTarget);
MySpawnedWeapon.SetOwner(this);
MyWeaponReference = Cast(MySpawnedWeapon);
MyWeaponReference.GetOwnerProperties(CharacterCamera);
MyWeaponReference.FireSignature.AddUFunction(this, n"WeaponFired");
MyWeaponReference.MagazineSignature.AddUFunction(this, n"StartReloadWeapon");
}
}
UFUNCTION(BlueprintEvent)
void WeaponFired(ACharacter OwnerCharacter)
{
if(bShakeCamera == true)
{
APlayerController PC = Cast(OwnerCharacter.GetController());
if(PC != nullptr)
{
PC.ClientPlayCameraShake(FireCamShake);
}
else
{
Print("Camera will not shake. Player Controller is NULL", 2.0f);
}
}
bIsFiring = true;
ServerFiringAnimation();
}
UFUNCTION(BlueprintEvent)
void FinishReloadWeapon()
{
MyWeaponReference.ReloadClip();
MyWeaponReference.bIsCurrentReloading = false;
bIsReloading = false;
}
/*
* Damage
*/
UFUNCTION(BlueprintOverride)
void RadialDamage(float DamageReceived, const UDamageType DamageType, FVector Origin, FHitResult HitInfo, AController InstigatedBy, AActor DamageCauser)
{
//DEBUG
if(CVar_DebugWeaponDrawing.GetInt() > 0)
{
System::DrawDebugString(Origin, FString("Radial Damage: " + DamageReceived) , nullptr, FLinearColor::White, 1.0f);
}
}
UFUNCTION(BlueprintOverride)
void PointDamage(float DamageReceived, const UDamageType DamageType, FVector HitLocation, FVector HitNormal, UPrimitiveComponent HitComponent, FName BoneName, FVector ShotFromDirection, AController InstigatedBy, AActor DamageCauser, FHitResult HitInfo)
{
//DEBUG
if(CVar_DebugWeaponDrawing.GetInt() > 0)
{
System::DrawDebugString(HitLocation, FString("PointDamage: " + DamageReceived) , nullptr, FLinearColor::White, 1.0f);
}
Gameplay::ApplyDamage(this, DamageReceived, InstigatedBy, DamageCauser, MyDamageType);
}
UFUNCTION(BlueprintEvent)
void DamageImpactAnimation(float Damage, float Health, const UDamageType DamageType, AController InstigatedBy, AActor DamageCauser)
{
return;
}
/*
* Networking
*/
UFUNCTION(Server)
void ServerSprint(float Value)
{
NewSpeed = Value;
}
UFUNCTION(Server)
void ServerCrouch(bool Crouching)
{
if(Crouching)
{
bIsCrouch = true;
}
else
{
bIsCrouch = false;
}
}
UFUNCTION(Server)
void ServerReload()
{
FKey Key;
StartReloadWeapon(Key);
}
UFUNCTION(Server)
void ServerFiringAnimation()
{
PlayFiringAnimation();
}
UFUNCTION(NetMulticast, BlueprintEvent)
void PlayFiringAnimation()
{
return;
}
UFUNCTION()
void OnRep_PlayerIsDead()
{
RemovePlayerWidgets();
}
UFUNCTION(BlueprintEvent)
void RemovePlayerWidgets()
{
return;
}
UFUNCTION(Server)
void ServerDisarm()
{
FKey Key;
Disarm(Key);
}
}
Health Component
import Core.COOPStatics;
event void FOnDealDamage(float Damage, float Health, const UDamageType DamageType, AController InstigatedBy, AActor DamageCauser);
event void FOnDead();
class UCOOPHealthComponent : UActorComponent
{
UPROPERTY(ReplicatedUsing=OnRep_Health, BlueprintReadOnly)
float Health;
UPROPERTY()
float MaxHealth = 500.0f;
UPROPERTY(Replicated)
bool bIsDead;
UPROPERTY()
FOnDealDamage OnDealDamageSignature;
UPROPERTY()
FOnDead OnDeadSignature;
default SetIsReplicated(true);
UFUNCTION(BlueprintOverride)
void BeginPlay()
{
if(GetOwner().LocalRole == ENetRole::ROLE_Authority)
{
AActor MyOwner = GetOwner();
if(MyOwner != nullptr)
{
MyOwner.OnTakeAnyDamage.AddUFunction(this, n"DealDamage");
}
}
Health = MaxHealth;
bIsDead = false;
}
UFUNCTION()
void DealDamage(AActor DamagedActor, float Damage, const UDamageType DamageType, AController InstigatedBy, AActor DamageCauser)
{
Health = Health - Damage;
if(Health > 0 && !bIsDead)
{
OnDealDamageSignature.Broadcast(Damage, Health, DamageType, InstigatedBy, DamageCauser);
//DEBUG
if(CVar_DebugHealthComponents.GetInt() > 0)
{
Print(FString(DamagedActor.GetName() + " Damage Taken: " + Damage), 1.0f, FLinearColor::Yellow);
}
}
else if(!bIsDead)
{
//DEBUG
if(CVar_DebugHealthComponents.GetInt() > 0)
{
Print(FString(DamagedActor.GetName() + " HealthComponent: Health is below 0"), 1.0f, FLinearColor::Yellow);
}
bIsDead = true;
OnDeadSignature.Broadcast();
}
}
UFUNCTION()
void OnRep_Health(float OldHealth)
{
float Damage = Health - OldHealth;
const UDamageType DamageType;
OnDealDamageSignature.Broadcast(Damage, Health, DamageType, GetOwner().GetInstigatorController(), GetOwner());
}
}