Pok'our

Game Info:

Roles: Game Programmer & Project Manager

Date: February 2020

Time: 4 Weeks

Team Size: 12 People (3 Programmers)

Genre: Platformer

Engine: Unreal

Version Control: Perforce

Code Language: C++

Pok´our is a fast paced speed runner that combines the old mesoamerican ball sport “Pok-Ta-Pok” and parkour.

The goal of the game is to run the course as fast as possible by mastering parkour moves and hitting hoops with the Pok-Ta-Pok ball.

Challenge: Create a game that’s a hybrid between at least two sports. (We choose parkour and Pok’ta’pok)

Other members:

Designers:

Sebastian Lund (https://www.sebastianlund.net/)
Johan Hyberg (https://www.johanhyberg.com/)
Ivan Bulygin

Programmers:

Uhuru McCray (https://uhurumccray.nu/)
Samuel Gustafsson ()

2D artists:

Fredrik Lindau (https://flindau.artstation.com/)
Vanessa Hedman (https://vanessahedman.myportfolio.com/)
Victor Bohlin ()

My Contributions:

Character movement mechanics

As one of our chosen sports for this game was parkour, we wanted the character to feel fast and the movement to feel free.

One of the most important things for this was the hurdle. This had to feel natural and get activated on the right moment as well as having the animation play at the right time.

Code example - HurdleComponent.cpp
#include "HurdleComponent.h"
#include "PlayerCharacter.h"
#include <Components/ActorComponent.h>
#include <Components/SceneComponent.h>
#include <GameFramework/Actor.h>
#include <DrawDebugHelpers.h>
#include <CollisionQueryParams.h>


UHurdleComponent::UHurdleComponent()
{
	HurdleStrength = 400.f;
	HurdlePitchAngle = 25.f;
}


void UHurdleComponent::SetPlayer(APlayerCharacter* Character)
{
	Player = Character;
}

void UHurdleComponent::TakeAction()
{
	if (Player)
	{
		FVector HurdleVelocity = GetHurdleVelocity();
		Player->LaunchCharacter(HurdleVelocity, true, true);
	}
}

FVector UHurdleComponent::GetHurdleVelocity()
{
	FRotator HurdleDirection = Player->GetActorRotation();
	HurdleDirection.Pitch += HurdlePitchAngle;
	return HurdleDirection.Vector() * HurdleStrength;
}

FVector UHurdleComponent::FindEdgeHurdleLocation(FVector HurdleStart, bool& HurdleHit, FHitResult& HurdleHitResult)
{
	FVector Direction = FVector::DownVector * DownTraceMultiplier;
	FVector StartTrace = GetStartTraceVector(HurdleStart, HurdleHitResult.ImpactPoint);
	FVector EndTrace = StartTrace + Direction;
	FCollisionQueryParams TraceParams(FName(TEXT("hurdle")), true, Player);
	FHitResult Hit(ForceInit);

	Player->GetWorld()->LineTraceSingleByChannel(Hit, StartTrace, EndTrace, ECollisionChannel::ECC_Pawn, TraceParams);

	return Hit.ImpactPoint;
}


FVector UHurdleComponent::GetStartTraceVector(FVector HurdleStart, FVector ImpactPoint)
{
	float X, Y, Z;
	Z = HurdleStart.Z;
	X = ImpactPoint.X;
	Y = ImpactPoint.Y;

	return Player->GetActorForwardVector() + FVector(X, Y, Z);
} 

Ball throwing

In this game the player can throw balls through rings and unlock alternative paths for the player to take or to reduce the time.

After some feedback the designers requested for the ball to return to the player after throwing it.

Code example - BallThrowComponent.cpp
#include "BallThrowComponent.h"
#include "GameFramework/Actor.h"
#include "Ball.h"
#include "Components/SphereComponent.h"
#include "PlayerCharacter.h"
#include <Engine/World.h>

UBallThrowComponent::UBallThrowComponent()
{
	bEditableWhenInherited = true;
	bBoomerangBall = true;
	ChargeMultiplier = 3000.0f;
	MinCharge = 0.4f;
	MaxCharge = 1.2f;
}

void UBallThrowComponent::InitalizeComponent(APlayerCharacter* InitPlayer)
{
	Player = InitPlayer;
}

void UBallThrowComponent::ThrowBall(FRotator BallRotator, float PlayerSpeed, float ThrowCharge, FVector SpawnLocation)
{
	if (!Player)
	{
		return;
	}

	FTransform BallTransform = CreateBallTransform(BallRotator, SpawnLocation);

	SetChargeWithinLimits(ThrowCharge);

	BeginSpawningBall(BallTransform);
	InitializeBall(PlayerSpeed, ThrowCharge);
	SpawnBallAtLocation(BallTransform, SpawnLocation);

	bBallThrown = true;
}

void UBallThrowComponent::ReturnBall()
{
	BallToThrow->ReturnBall();
}

void UBallThrowComponent::SetBallThrown(bool bThrown)
{
	bBallThrown = bThrown;
}

bool UBallThrowComponent::IsBallThrown()
{
	return bBallThrown;
}

FTransform UBallThrowComponent::CreateBallTransform(FRotator BallRotator, FVector SpawnLocation)
{
	return FTransform(Player->GetCameraRotation(), SpawnLocation);
}

void UBallThrowComponent::SetChargeWithinLimits(float& ThrowCharge)
{
	if (ThrowCharge < MinCharge)
	{
		ThrowCharge = MinCharge;
	}
	else if (ThrowCharge > MaxCharge)
	{
		ThrowCharge = MaxCharge;
	}
}

void UBallThrowComponent::BeginSpawningBall(FTransform BallTransform)
{
	UWorld* World = GetWorld();
	AActor* Owner = GetOwner();

	BallToThrow = World->SpawnActorDeferred<ABall>(BallClass, BallTransform, Owner, Owner->Instigator);
}

void UBallThrowComponent::InitializeBall(float PlayerSpeed, float ThrowCharge)
{
	BallToThrow->Init(PlayerSpeed + (ThrowCharge * ChargeMultiplier), Player, bBoomerangBall);
}

void UBallThrowComponent::SpawnBallAtLocation(FTransform BallTransform, FVector SpawnLocation)
{
	BallToThrow->FinishSpawning(BallTransform);
	BallToThrow->SetActorLocation(SpawnLocation);
} 
Code example - Ball.cpp
#include "Ball.h"
#include "GameFramework/ProjectileMovementComponent.h"
#include "Components/SphereComponent.h"
#include "Engine.h"
#include "EngineGlobals.h"
#include "Kismet/KismetMathLibrary.h"
#include "PlayerCharacter.h"
#include "TimerManager.h"
#include "PlayerCharacter.h"
#include "BallThrowComponent.h"


ABall::ABall()
{
	SetupCollisionComponent();

	SetupProjectileMovementComponent();
}

void ABall::Tick(float DeltaTime)
{

	BallTick();
	if (bShouldReturn)
	{
		FVector TargetLocation = Player->BallThrowingLocation->GetComponentLocation();
		FVector CurrentLocation = GetActorLocation();
		FVector Movement;

		Movement = FMath::VInterpConstantTo(CurrentLocation, TargetLocation, DeltaTime, InitialSpeed * BallReturnSpeed);

		SetActorLocation(Movement);

	}
}

void ABall::OnOverlapBegin(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	if (OtherActor != nullptr)
	{
		auto Actor = Cast<APlayerCharacter>(OtherActor);

		if (Actor != nullptr && !GetWorld()->GetTimerManager().IsTimerActive(BallReturn))
		{
			UBallThrowComponent* BallThrowComponent = Actor->GetBallThrowComponent();

			if (BallThrowComponent != nullptr)
			{
				BallThrowComponent->SetBallThrown(false);
				Actor->CatchBall();
			}

			Destroy();
		}

	}
}

void ABall::SetupCollisionComponent()
{
	CollisionComp = CreateDefaultSubobject<USphereComponent>(TEXT("SphereComp"));
	CollisionComp->InitSphereRadius(5.0f);
	CollisionComp->BodyInstance.SetCollisionProfileName("Projectile");
	CollisionComp->OnComponentBeginOverlap.AddDynamic(this, &ABall::OnOverlapBegin);

	CollisionComp->SetWalkableSlopeOverride(FWalkableSlopeOverride(WalkableSlope_Unwalkable, 0.f));
	CollisionComp->CanCharacterStepUpOn = ECB_No;

	RootComponent = CollisionComp;
}

void ABall::SetupProjectileMovementComponent()
{
	ProjectileMovement = CreateDefaultSubobject<UProjectileMovementComponent>(TEXT("ProjectileComp"));
	ProjectileMovement->UpdatedComponent = CollisionComp;
	ProjectileMovement->InitialSpeed = InitialSpeed;
	ProjectileMovement->MaxSpeed = InitialSpeed;
	ProjectileMovement->bRotationFollowsVelocity = true;
}

void ABall::ReturnToPlayer()
{
	ProjectileMovement->Velocity = FVector::ZeroVector;
	ProjectileMovement->ProjectileGravityScale = 0.0f;
	CollisionComp->SetAllPhysicsLinearVelocity(FVector::ZeroVector);
	CollisionComp->SetSimulatePhysics(false);
	CollisionComp->SetEnableGravity(false);
	CollisionComp->SetCollisionResponseToAllChannels(ECR_Overlap);
	CollisionComp->SetCollisionResponseToChannel(ECC_Pawn, ECollisionResponse::ECR_Block);

	bShouldReturn = true;
	BallReturnTime = 0;
}

void ABall::Init(float Velocity, APlayerCharacter* InitPlayer, bool bBallIsBoomerang)
{
	Player = InitPlayer;
	FTimerManager& TimerManager = GetWorld()->GetTimerManager();
	ProjectileMovement->InitialSpeed = Velocity;
	ProjectileMovement->MaxSpeed = Velocity * VelocityMultiplier;
	CollisionComp->SetCollisionResponseToAllChannels(ECR_Ignore);

	if (!bBallIsBoomerang)
	{
		this->SetLifeSpan(BallLifetime);
		bShouldReturn = false;
	}
	else
	{
		TimerManager.SetTimer(BallReturn, this, &ABall::ReturnToPlayer, BallLifetime, false);
		bShouldReturn = false;
	}

	TimerManager.SetTimer(BallCollisionHandle, this, &ABall::EnableCollision, 0.2f, false);
}

void ABall::ReturnBall()
{
	GetWorld()->GetTimerManager().ClearTimer(BallReturn);
	ReturnToPlayer();
}

bool ABall::CheckForCollisionPhysics(AActor* OtherActor, UPrimitiveComponent* OtherComp)
{
	return (OtherActor != nullptr) && (OtherActor != this) && (OtherComp != nullptr);
}

void ABall::EnableCollision()
{
	CollisionComp->SetCollisionResponseToAllChannels(ECR_Block);
	ProjectileMovement->bShouldBounce = true;
} 
Code example - PlayerCharacter.cpp
void APlayerCharacter::BeginBallThrow()
{
	if (BallThrowComponent->IsBallThrown() && BallThrowComponent->bBoomerangBall)
	{
		BallThrowComponent->ReturnBall();
		bIsBallReturning = true;
		return;
	}

	BallHoldStartTime = GetWorld()->TimeSeconds;
	BallThrowComponent->OnBeginThrow(BallThrowingLocation->GetComponentLocation());
	bChargingThrow = true;
}

void APlayerCharacter::ThrowBall()
{
	if (!bChargingThrow)
	{
		return;
	}

	float Yaw;
	float Pitch;
	FRotator CameraRotator;

	if (FirstPersonCameraComponent->IsActive())
	{
		CameraRotator = GetActiveCameraComponent()->GetComponentRotation();
		Yaw = CameraRotator.Yaw;
		Pitch = CameraRotator.Pitch;
	}
	else
	{
		CameraRotator = ThirdPersonCameraSpringArm->GetComponentRotation();
	}

	float TotalCharge = GetWorld()->TimeSeconds - BallHoldStartTime;

	BallThrowComponent->ThrowBall(CameraRotator, GetCurrentSpeed(), TotalCharge, BallThrowingLocation->GetComponentLocation());
	bChargingThrow = false;
	BallReturnCapsule->SetActive(true);
	BallOnCharacter->SetVisibility(false);
	bIsHoldingBall = false;
}