Snowscape

Duration:  4 Weeks

Team size: 13 People

Language: C++, Bluerint

Engine: Unreal engine

My role: Gameplay programmer,

Scrum master, Co Project Owner


This was my second project at Futuregames and our goal was to make a game based on two different sports.

The sports we chosen to combine was Pole Vaulting and Snowboarding. The game was developed in Unreal engine where we used both Blueprint and C++.


Snowscape is a linear single-player adventure game, set in the snowed-in ruins of an ancient society. It is an expressive journey, through a forgotten world.


In the game, there is no combat. Instead, we aimed to bring the atmosphere to the forefront, through smooth mechanics and a captivating ambience.


The goal is to reach an ancient temple and restore it to its former glory, relighting its fires and rediscovering it's past.


The player uses a snowboard and a vaulting pole to reach the finish line. These two mechanics ties into how you build momentum, and lets you experience the world.

My roles in this project were gameplay programmer, scrum master and co project owner together with another team member.

The Project owner role taught me how to structure a plan according to the different disciplines that aimed at the same end goal. It also improved my skills for conflict resolving.

The first week was hard and I messed up the structure by not planning for possible issues ahead. But I learned from my mistakes and the rest of the project went better.

I did not realize how hard it could be to guide a team to the right direction, but I am glad to be given this experience to avoid making the same mistakes in the future.

My role as scrum master went much better even though I had no practical experience about scrum, I was fortunate to get help from a relative who teaches scrum and agile working on a professional level.

In our team we were 13 studentsat FutureGames, 3 programmer, 3 game designers, 3 2D artists and 4 3D artists. This project was interesting because it was the first project where we worked in Unreal engine.

We were a large team, which I realized, was a huge difference from my last project of 6 people. Mainly because we were twice as many resources and had twice the amount of time. This also made me realize the importance of understanding each other’s work and strive for the same vision.

We scheduled playtests together, and we made sure that we took at least one lap around the workstations every day to see what everyone was currently working with.

For this project we set up a detailed trello board, since it was so detailed it helped us to keep track of what everyone were doing and was a helpful tool. Our daily standups for this project were time boxed which made us productive.

During the second week we encountered a conflict between programmers and designers where both disciplines had a hard time with dividing tasks. We solved the issue by immediately addressing the problem and sat down to define what each discipline was responsible for.

We had a democratic approach and it worked well for us as a team. I believe we communicated well and solved issues fast.

The programming part 

Since this was an unreal project, we chose to combine blueprints and code. The scripts that I worked with mostly was the Player movement and the base of Collectibles.


For blueprints I worked mostly on the collisions and respawn systems. We were three programmers working with this project and we all worked closely together and many of the systems was a team effort with pair programming.


We also worked close with the members in art and game design. This project was challenging at times and some issues that I had was to combine blueprints and C++ code.


I had almost no experience of C++, unreal and blueprints. But it was a fun learning experience, and I got a better overview about C++ and programming overall. The blueprints were a good way for me to get a better understanding visually how a script is executed.


For this script I made a base for all our pickups. I struggled a bit with connecting it to blueprints but I researched and watched tutorials to get a better understanding of how to do it. After the base was done another programmer in my team continued and created different properties for the pickups.

      
      	
#include "Collectibles.h"
#include "Components/StaticMeshComponent.h"
#include "Runtime/Engine/Classes/Kismet/GameplayStatics.h"
#include <Components/BoxComponent.h>
#include <Engine/Engine.h>
#include <GameFramework/WorldSettings.h>

ACollectibles::ACollectibles()
{
	PrimaryActorTick.bCanEverTick = true;

	PickupRoot = CreateDefaultSubobject<USceneComponent>(TEXT("PickupRoot"));
	RootComponent = PickupRoot;

	PickupBox = CreateDefaultSubobject<UBoxComponent>(TEXT("PickupBox"));
	PickupBox->SetWorldScale3D(FVector(1.0f, 1.0f, 1.0f));
	PickupBox->SetGenerateOverlapEvents(true);
	PickupBox->OnComponentBeginOverlap.AddDynamic(this, &ACollectibles::OnPlayerEnterPickupBox);
	PickupBox->SetupAttachment(PickupRoot);
}

void ACollectibles::BeginPlay()
{
	Super::BeginPlay();
	PlayerMovement = Cast<APlayerMovement</APlayerMovement(UGameplayStatics::GetPlayerCharacter(GetWorld(), 0));
	NormalSnowboardSpeed = PlayerMovement->SnowboardSpeedcpp;

	if (bHardCodeSetup)
	{
		bRespawnPickup = true;
		bDestroyOnPickup = false;
		RespawnTime = 3.0f;
	}
}

void ACollectibles::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
}

// On pickup
void ACollectibles::OnPlayerEnterPickupBox(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{

	if (PlayerMovement == OtherActor)
	{
		if (bSpeedBoost)
		{
			if (!PlayerMovement->Snowboard->bIsSnowboardActive)
			{
				PlayerMovement->SwitchToSnowboard();
				SpeedBoostOnFoot();
				GetWorld()->GetTimerManager().SetTimer(TH_TickTimer, this, &ACollectibles::SpeedBoostOnFoot, 0.5f, true);
			}

			else if (PlayerMovement->Snowboard)
			{
				SpeedBoost();
				GetWorld()->GetTimerManager().SetTimer(TH_TickTimer, this, &ACollectibles::SpeedBoost, 0.5f, true);
			}
		}

		if (bTimeScale)
		{

			GetWorld()->GetTimerManager().SetTimer(TH_TickTimer, this, &ACollectibles::TimeScale, 0.002f, true);
		}

		if (bUpWind) { UpWind(); }
		if (bShrink) { Shrink(); }



		GetWorld()->GetTimerManager().SetTimer(TH_EffectTime, this, &ACollectibles::ResetEffects, EffectTime, false);

		// Hide if it's a destroyable pickup (will destroy on reset)
		if (bDestroyOnPickup || bRespawnPickup)
		{
			SetActorHiddenInGame(true);
		}
	}
}

// Pickup effects
void ACollectibles::SpeedBoost()
{
	PlayerMovement->SnowboardBoostcpp += BoostPower;
}

void ACollectibles::SpeedBoostOnFoot()
{
	//GEngine->AddOnScreenDebugMessage(-1, 1.5, FColor::Green, TEXT("Slowboost because on foot"));
	PlayerMovement->SnowboardBoostcpp += BoostPowerHalf;
}

// TimeScale
void ACollectibles::TimeScale()
{
	if (!bTimesIsScalingBack)
	{
		GetWorld()->GetWorldSettings()->SetTimeDilation(TimeDilationSet);

		if (TimeDilationSet > TimeDilation)
		{
			TimeDilationSet -= 0.01f;
		}
		if (TimeDilationSet <= TimeDilation)
		{
			bTimesIsScalingBack = true;
			GetWorld()->GetTimerManager().ClearTimer(TH_TickTimer);
			GetWorld()->GetTimerManager().SetTimer(TH_TickTimer, this, &ACollectibles::TimeScaleBack, 0.0015f, true);
			//TimeScaleBack();
		}
	}

}

void ACollectibles::TimeScaleBack()
{
	GetWorld()->GetWorldSettings()->SetTimeDilation(TimeDilationSet);
	if (TimeDilationSet < 1.0f)
	{
		TimeDilationSet += 0.01f;
	}
}

void ACollectibles::UpWind()
{

	PlayerMovement->GetCharacterMovement()->AddForce(FVector(0.0f, 0.0f, 100.0f));
}

void ACollectibles::Shrink()
{
	// smooth this
	PlayerMovement->GetMesh()->SetWorldScale3D(FVector(0.25f, 0.25f, 0.25f));
	PlayerMovement->OnShrink();
}

// Reset all effects here
void ACollectibles::ResetEffects()
{

	GetWorld()->GetTimerManager().ClearTimer(TH_EffectTime);
	GetWorld()->GetTimerManager().ClearTimer(TH_TickTimer);

	// Reset
	PlayerMovement->SnowboardBoostcpp = 0.0f;
	PlayerMovement->OnBoostComplete();
	PlayerMovement->OnShrinkComplete();
	PlayerMovement->GetMesh()->SetWorldScale3D(FVector(1.0f, 1.0f, 1.0f));
	GetWorld()->GetWorldSettings()->SetTimeDilation(1.0f);

	// Clear timers

	// Destroy
	if (!bRespawnPickup && bDestroyOnPickup)
	{
		Destroy();
	}
	else if (bRespawnPickup)
	{
		GetWorld()->GetTimerManager().SetTimer(TH_TickTimer, this, &ACollectibles::RespawnIsh, RespawnTime, false);
	}
}

void ACollectibles::RespawnIsh()
{
	SetActorHiddenInGame(false);
}


      	
      
    

I had a little experience of making a player movement script before in unity, so it was a bit easier for me to find information on how to work with it in unreal C++ and blueprint. This script holds the player input and the functions for switching between the pole and walking input. This is connected to the movement blueprint down below.

      
      	
#include "PlayerMovement.h"
#include "SnowboardComponent.h"
#include "Kismet/GameplayStatics.h"
#include "TimerManager.h"
#include "Engine/World.h"
#include "Checkpoints.h"
#include "Kismet/GameplayStatics.h"
#include "DrawDebugHelpers.h"
#include <Engine/Engine.h>
#include <GameFramework/Character.h>


APlayerMovement::APlayerMovement()
{
	PrimaryActorTick.bCanEverTick = true;
	WalkSpeed = GetCharacterMovement()->MaxWalkSpeed;

	bUseControllerRotationPitch = false;
	bUseControllerRotationYaw = false;
	bUseControllerRotationRoll = false;

	GetCharacterMovement()->bOrientRotationToMovement = true;
	GetCharacterMovement()->RotationRate = FRotator(0.0f, 540.0f, 0.0f);
	GetCharacterMovement()->JumpZVelocity = 1000.0f;
	GetCharacterMovement()->AirControl = 0.6f;
	CurrentState = EPlayerStates::ThirdPerson;
	Snowboard = CreateDefaultSubobject<USnowboardComponent>(TEXT("Snowboard"));



}

void APlayerMovement::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
	if (bIsJumping)
	{
		if (!bIsGrounded)
		{
			return;
		}
		else if (!bIsGrounded && bHasJumped)
		{
			return;
		}
		else if (bIsGrounded && bHasJumped)
		{

			OnLand();
		}
	}
}

void APlayerMovement::BeginPlay()
{
	Super::BeginPlay();
	ResetToThisJumpForce = VaultJumpForce;
}


void APlayerMovement::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

	// Save game
	InputComponent->BindAction("Save", IE_Pressed, this, &APlayerMovement::SaveGame);
	InputComponent->BindAction("Load", IE_Pressed, this, &APlayerMovement::LoadGame);

	// MouseLook
	PlayerInputComponent->BindAxis("Turn", this, &APawn::AddControllerYawInput);
	PlayerInputComponent->BindAxis("LookUp", this, &APawn::AddControllerPitchInput);

	// Movement
	/*PlayerInputComponent->BindAction("Jump", IE_Pressed, this, &ACharacter::Jump);
	PlayerInputComponent->BindAction("Jump", IE_Released, this, &ACharacter::StopJumping);*/
	PlayerInputComponent->BindAxis("MoveForward", this, &APlayerMovement::MoveForward);
	PlayerInputComponent->BindAxis("MoveRight", this, &APlayerMovement::MoveRight);
	/*PlayerInputComponent->BindAction("Vault", IE_Pressed, this, &APlayerMovement::PoleVaultPrepare);
	PlayerInputComponent->BindAction("Vault", IE_Released, this, &APlayerMovement::PoleVaultExecute);*/

	// Other Inputs
	PlayerInputComponent->BindAction("Snowboard", IE_Pressed, this, &APlayerMovement::SwitchToSnowboard);
}

void APlayerMovement::MoveForward(float Axis)
{
	if (CurrentState == EPlayerStates::ThirdPerson)
	{
		FRotator Rotatation = Controller->GetControlRotation();
		FRotator YawRotataion(0.0f, Rotatation.Yaw, 0.0f);

		FVector Direction = FRotationMatrix(YawRotataion).GetUnitAxis(EAxis::X);
		AddMovementInput(Direction, Axis);
	}
}

void APlayerMovement::MoveRight(float Axis)
{
	if (CurrentState == EPlayerStates::ThirdPerson)
	{
		FRotator Rotatation = Controller->GetControlRotation();
		FRotator YawRotataion(0.0f, Rotatation.Yaw, 0.0f);

		FVector Direction = FRotationMatrix(YawRotataion).GetUnitAxis(EAxis::Y);
		AddMovementInput(Direction, Axis);
	}
}

void APlayerMovement::SwitchToSnowboard()
{
	if (Snowboard)
	{
		Snowboard->SwitchSnowboardActive();

		if (Snowboard->bIsSnowboardActive)
		{
			SwitchTargetState(EPlayerStates::Snowboard);
		}
		else if (!Snowboard->bIsSnowboardActive)
		{
			SwitchTargetState(EPlayerStates::ThirdPerson);
		}
		SwitchState(TargetState);
	}
}

void APlayerMovement::PoleVaultPrepare()
{
	VaultJumpForce = ResetToThisJumpForce;
	GetCharacterMovement()->JumpZVelocity = BaseJumpForce;
	GetWorld()->GetTimerManager().SetTimer(TH_VaultChargeTimer, this, &APlayerMovement::ChargeJump, 0.1f, true);
}

void APlayerMovement::PoleVaultExecute()
{
	bIsJumping = true;
	bHasJumped = true;

	if (CurrentState == EPlayerStates::Snowboard)
	{
		SwitchTargetState(EPlayerStates::Snowboard);
		SwitchState(EPlayerStates::SnowboardPolevault);
	}
	else if (CurrentState == EPlayerStates::ThirdPerson)
	{
		SwitchTargetState(EPlayerStates::ThirdPerson);
		SwitchState(EPlayerStates::ThirdPersonPolevault);
	}

	GetWorld()->GetTimerManager().ClearTimer(TH_VaultChargeTimer);
	GetCharacterMovement()->JumpZVelocity = VaultJumpForce;

	// delay to wait for animation to complete
	APlayerMovement::Jump();

	// Delay before resetting jumpvelocity
	GetWorld()->GetTimerManager().SetTimer(TH_JumpReset, this, &APlayerMovement::ResetJump, 0.5f, false);
}


EPlayerStates APlayerMovement::ReturnCurrentState_Implementation() const
{
	return CurrentState;
}
void APlayerMovement::OnSwitchState_Implementation()
{

}
void APlayerMovement::OnLand_Implementation()
{
	SwitchState(TargetState);
	bHasJumped = false;
	bIsJumping = false;
	ReturnCurrentState();
}

void APlayerMovement::ForceSwitchState(EPlayerStates ForceState)
{

	SwitchState(ForceState);
}

void APlayerMovement::ChargeJump()
{
	// timer
	if (VaultJumpForce <= MaxVaultJumpForce)
	{
		//VaultJumpForce += ChargeSpeedMultiplyer;
	}
}

void APlayerMovement::ResetJump()
{
	GetCharacterMovement()->JumpZVelocity = BaseJumpForce;
}

//Pick up and drop stuff
void APlayerMovement::Climb()
{
	bIsClimbing = true;
	if (bIsClimbing)
	{
		GetCharacterMovement()->SetActive(false);
	}
}

void APlayerMovement::StopClimb()
{
	if (bIsClimbing)
	{
		bIsClimbing = false;
		GetCharacterMovement()->SetActive(true);
		APlayerMovement::ClimbJump();
	}
}

void APlayerMovement::ClimbJump()
{
	GetCharacterMovement()->Velocity = FVector(ClimbJumpForward, 0.0f, ClimbJumpUp);
}

void APlayerMovement::SwitchState(EPlayerStates TargetState)
{
	if (CurrentState != TargetState)
	{
		CurrentState = TargetState;
	}
	OnSwitchState();
}

void APlayerMovement::SwitchTargetState(EPlayerStates TargetState)
{
	this->TargetState = TargetState;
}

void APlayerMovement::SaveGame()
{

	UCheckpoints* SaveGameInstance = Cast<UCheckpoints>(UGameplayStatics::CreateSaveGameObject(UCheckpoints::StaticClass()));

	SaveGameInstance->PlayerLocation = this->GetActorLocation();
	SaveGameInstance->PlayerRotation = this->GetActorRotation();

	UGameplayStatics::AsyncSaveGameToSlot(SaveGameInstance, TEXT("NewSlot"), 0);
}

void APlayerMovement::LoadGame()
{
	UCheckpoints* SaveGameInstance = Cast<UCheckpoints>(UGameplayStatics::CreateSaveGameObject(UCheckpoints::StaticClass()));

	SaveGameInstance = Cast<UCheckpoints>(UGameplayStatics::LoadGameFromSlot("NewSlot", 0));

	this->SetActorLocation(SaveGameInstance->PlayerLocation);
	this->SetActorRotation(SaveGameInstance->PlayerRotation);
}


void APlayerMovement::OnBoost_Implementation()
{

}

void APlayerMovement::OnBoostComplete_Implementation()
{

}

void APlayerMovement::OnShrink_Implementation()
{
}

void APlayerMovement::OnShrinkComplete_Implementation()
{
}


      	
      
    

The movement blueprint is connected to the movement script above that holds functions for switching between the inputs for the pole vault jumping and movement.

The two collision blueprint is connectedwith each other and with the respawn function which is a trigger box that loads a save from the last position where another trigger box has a function of auto saving the game on collision.