*Przedstawiam analizę tylko tych części projektu nad którymi pracowałem.
Spis Treści
1. Wstęp
Dołączyłem do zespołu który utworzył człowiek z którym współpracowałem nad projektem PrototypeH. Zespół składał się z 26 osób, a z czasem malał finalnie do 15 osób. Pracowaliśmy 30 dni nad projektem na game jam „Game off 2023” organizowany przez Github. Po wstępnym zapoznaniu się przez wszystkich uczestników postanowiliśmy utworzyć działy: Programming, Art oraz Sound na czele których miał stać Head of Dept/Lead. Człowiek dzięki któremu dołączyłem został Project managerem, a ja ku mojemu zaskoczeniu z racji największej wiedzy i doświadczenia zostałem Programming Lead’em/Head Of Programming Dept. Byłem odpowiedzialny za pracę całego działu, wybór frameworków, architekturę projektu oraz zarządzanie Source Control.
W trakcie całego projektu zrobiliśmy kilka zebrań na których początkowo planowaliśmy podział pracy, design gry, przedstawialiśmy nasze pomysły i rozwiązania po poznaniu motywu „Scales”. Z czasem zebrania miały charakter informacyjny, przedstawialiśmy dotychczasowe postępy naszych działów, dalszy rozwój, feedback ukończonych elementów jak i terminy, optymalne sposoby na scalenie prac między działami.
Finałem naszej pracy była gra „Dragon’s Veil” – trzecioosobowy soulslike inspirowany takimi grami jak seria Dark Souls, Elden Ring, czy Lords of the fallen.
2. Wytyczne projektu
Po otrzymaniu motywu i brain stormingu, PM przygotował wytyczne projektu. Miał to być soulslike z perspektywy trzeciej osoby, miał posiadać skille takie jak ataki, dodge, ogniska pełniące role checkpointów dzięki którym gracz mógł zapisywać swój postęp, zwykłych wrogów oraz smoka który pełni rolę bossa. Bronie, armor, przedmioty z którymi można przeprowadzać interakcję, można się w nie wyposażyć, które bezpośrednio zmieniają statystyki takie jak armor, damage, health, stamina. Przygotowany był dokument w stylu GDD do którego niestety nie mam dostępu.
3. Source control
Wymogiem przy użyciu source control było korzystanie z github oraz publiczne repozytorium danych przypisane do prywatnego konta.

Był to mój pierwszy projekt w którym musiałem zarządzać repozytorium przy tak licznym zespole. Udałem się po poradę do mojego znajomego Senior Java architect’a, pytałem jak wygląda Source Control przy większych projektach. Wyszło na to, że w IT pracuje się aktualnie używając Feature branches. Osoba tworzy osobny branch z main’a dodaje swój feature i z powrotem merguje z main’em. Ważne żeby takie feature’y były implementowane często i względnie szybko, w ten sposób minimalizuje się ryzyko konfliktów. Dodatkowo przy pracy na plikach binarnych ważne było żeby deklarować przy czym obecnie się pracuje, gdyż w gicie z tego co wiem nie da się standardowo zamykać plików wykluczając przy tym breaching przez pozostałych członków – z tym poradziliśmy sobie poprzez utworzenie listy kanban w github projects.
Dzięki temu zabiegowi zawsze było wiadomo kto i przy czym obecnie pracuje, co jest aktualnie w kolejce do main’a itp.

Byłem także odpowiedzialny za akceptowanie pull requestów i ewentualne fix’y związane z merge conflicts. Plików binarnych nie da się edytowac/modyfikować poprzez code review więc używałem do tego metody Cherry picking komunikując się z autorami commit’ów oraz stosując strategie theirs lub ours w zależności od rodzaju konfliktu w plikach. Czasami problem był związany z tym, że osoba commitująca niechcący nadpisała plik przy którym nie pracowała, a czasami modyfikowała klasę której nie powinna była. Musiałem oceniać ryzyko oraz ilość strat związanych z mergem i zastosować odpowiednią strategię cherry picking’u.
4. Frameworki, architektura, Rozwiązania
Znając wytyczne i to co ma zostać zawarte w projekcie przystąpiłem do wyboru, implementacji frameworków, a także do utworzenia szkieletu projektu kategoryzując foldery.




4.1 Gameplay Ability System (GAS)
Najważniejszym frameworkiem w przypadku tego projektu był 'GAS’. Gameplay ability system jest domyślnie dostępny w Unreal Engine i jest wykorzystywany np. w grze Fortnite. Odpowiada on za umiejętności oraz statystyki. Umiejętnością może być cokolwiek, np. otworzenie drzwi, sprint czy atak mieczem. Statystyki również nie ograniczają się do poziomu życia czy staminy, statystyką może być np. armor, level, prędkość chodzenia czy ilość udźwigu w ekwipunku postaci. Najbardziej dogodnym ułatwieniem jest fakt, że umiejętności są co prawda inicjowane z poziomu klasy gracza, ale każda z nich jest osobną klasą co pozwala na swobodną pracę programistów i podziałem pracy w source control.
GAS należy najpierw zaimplementować w C++, a potem można bez problemu korzystać z blueprint’ów. Utworzyłem w tym zamyśle w cpp klasę bazową postaci, a także Attribute Set i Ability System Component.
4.1.1 C++
Dodałem do pliku *.Build.cs linijkę kodu która odpowiada za deklarację używanych modułów.
PrivateDependencyModuleNames.AddRange(new string[] { "GameplayAbilities", "GameplayTags", "GameplayTasks" });W GASAttributeSet.h zadeklarowałem statystyki które będzie posiadała postać
// Author: Robert Wesoły
#pragma once
#include "CoreMinimal.h"
#include "AttributeSet.h"
#include "AbilitySystemComponent.h"
#include "GASAttributeSet.generated.h"
#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)
UCLASS()
class GAMEOFF2023PROJECT_API UGASAttributeSet : public UAttributeSet
{
GENERATED_BODY()
public:
UGASAttributeSet();
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
virtual void PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data) override;
//virtual void PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) override;
UPROPERTY(BlueprintReadOnly, Category = "Attributes")
FGameplayAttributeData Health;
ATTRIBUTE_ACCESSORS(UGASAttributeSet, Health);
UFUNCTION()
virtual void OnRep_Health(const FGameplayAttributeData& OldHealth);
UPROPERTY(BlueprintReadOnly, Category = "Attributes")
FGameplayAttributeData MaxHealth;
ATTRIBUTE_ACCESSORS(UGASAttributeSet, MaxHealth);
UFUNCTION()
virtual void OnRep_MaxHealth(const FGameplayAttributeData& OldMaxHealth);
//----------------------------
UPROPERTY(BlueprintReadOnly, Category = "Attributes")
FGameplayAttributeData Stamina;
ATTRIBUTE_ACCESSORS(UGASAttributeSet, Stamina);
UFUNCTION()
virtual void OnRep_Stamina(const FGameplayAttributeData& OldStamina);
UPROPERTY(BlueprintReadOnly, Category = "Attributes")
FGameplayAttributeData MaxStamina;
ATTRIBUTE_ACCESSORS(UGASAttributeSet, MaxStamina);
UFUNCTION()
virtual void OnRep_MaxStamina(const FGameplayAttributeData& OldMaxStamina);
//----------------------------
UPROPERTY(BlueprintReadOnly, Category = "Attributes")
FGameplayAttributeData Armor;
ATTRIBUTE_ACCESSORS(UGASAttributeSet, Armor);
UFUNCTION()
virtual void OnRep_Armor(const FGameplayAttributeData& OldStamina);
public:
UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Category = "Abilities")
TSubclassOf<class UGameplayEffect> DefaultAttributeEffect;
UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Category = "Abilities")
TArray<TSubclassOf<class UGameplayAbility>> DefaultAbilities;
};
Następnie utworzyłem definicje w GASAttributeSet.cpp
// Author: Robert Wesoły
#include "GASAttributeSet.h"
#include "GameplayEffect.h"
#include "GameplayEffectExtension.h"
#include "Net/UnrealNetwork.h"
#include "Containers/UnrealString.h"
UGASAttributeSet::UGASAttributeSet()
{
}
void UGASAttributeSet::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
DOREPLIFETIME_CONDITION_NOTIFY(UGASAttributeSet, Health, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UGASAttributeSet, Stamina, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UGASAttributeSet, MaxHealth, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UGASAttributeSet, MaxStamina, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UGASAttributeSet, Armor, COND_None, REPNOTIFY_Always);
}
void UGASAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
Super::PostGameplayEffectExecute(Data);
//GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, TEXT("This is an on screen message2!"));
if (Data.EvaluatedData.Attribute == GetHealthAttribute())
{
SetHealth(FMath::Clamp(GetHealth(), 0.0f, GetMaxHealth()));
//GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, TEXT("HP2"));
}
else if (Data.EvaluatedData.Attribute == GetStaminaAttribute())
{
SetStamina(FMath::Clamp(GetStamina(), 0.0f, GetMaxStamina()));
//GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, TEXT("Stam2"));
}
}
void UGASAttributeSet::OnRep_Stamina(const FGameplayAttributeData& OldStamina)
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UGASAttributeSet, Stamina, OldStamina);
}
void UGASAttributeSet::OnRep_MaxStamina(const FGameplayAttributeData& OldMaxStamina)
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UGASAttributeSet, MaxStamina, OldMaxStamina);
}
void UGASAttributeSet::OnRep_Health(const FGameplayAttributeData& OldHealth)
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UGASAttributeSet, Health, OldHealth);
}
void UGASAttributeSet::OnRep_MaxHealth(const FGameplayAttributeData& OldMaxHealth)
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UGASAttributeSet, MaxHealth, OldMaxHealth);
}
void UGASAttributeSet::OnRep_Armor(const FGameplayAttributeData& OldHealth)
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UGASAttributeSet, Armor, OldHealth);
}W utworzonej klasie bazowej postaci MyAGRCharacter.h zaimplementowałem AbilitySystemInterface, zmienne Int, zadeklarowałem funkcje oraz utworzyłem tablicę podstawowych umiejętności
// Author: Robert Wesoły
#pragma once
#include "CoreMinimal.h"
#include "Characters/AGRCharacter.h"
#include "GameplayTagContainer.h"
#include <GameplayEffectTypes.h>
#include "AbilitySystemInterface.h"
#include "GASAttributeSet.h"
#include "MyAGRCharacter.generated.h"
UCLASS()
class GAMEOFF2023PROJECT_API AMyAGRCharacter : public AAGRCharacter, public IAbilitySystemInterface
{
GENERATED_BODY()
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Abilities, meta = (AllowPrivateAccess = "true"))
class UAbilitySystemComponent* AbilitySystemComponent;
UPROPERTY()
class UGASAttributeSet* Attributes;
public:
AMyAGRCharacter();
// overriden from IAbilitySystemInterface
UAbilitySystemComponent* GetAbilitySystemComponent() const override;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Attributes")
int MaxHealth = 100;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Attributes")
int MaxStamina = 100;
public:
virtual void PossessedBy(AController* NewController) override;
virtual void OnRep_PlayerState() override;
virtual void InitializeAttributes();
virtual void GiveDefaultAbilities();
UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Category = "Abilities")
TSubclassOf<class UGameplayEffect> DefaultAttributeEffect;
UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Category = "Abilities")
TArray<TSubclassOf<class UGameplayAbility>> DefaultAbilities;
};
W MyAGRCharacter.cpp utworzyłem definicje funkcji
// Author: Robert Wesoły
#include "MyAGRCharacter.h"
#include "AbilitySystemComponent.h"
#include "GASAttributeSet.h"
AMyAGRCharacter::AMyAGRCharacter()
{
AbilitySystemComponent = CreateDefaultSubobject<UAbilitySystemComponent>("AbilitySystemComp");
AbilitySystemComponent->SetIsReplicated(true);
AbilitySystemComponent->SetReplicationMode(EGameplayEffectReplicationMode::Minimal);
Attributes = CreateDefaultSubobject<UGASAttributeSet>("Attributes");
}
UAbilitySystemComponent* AMyAGRCharacter::GetAbilitySystemComponent() const
{
return AbilitySystemComponent;
}
void AMyAGRCharacter::PossessedBy(AController* NewController)
{
Super::PossessedBy(NewController);
if (AbilitySystemComponent)
AbilitySystemComponent->InitAbilityActorInfo(this, this);
InitializeAttributes();
GiveDefaultAbilities();
}
void AMyAGRCharacter::OnRep_PlayerState()
{
Super::OnRep_PlayerState();
if (AbilitySystemComponent)
AbilitySystemComponent->InitAbilityActorInfo(this, this);
InitializeAttributes();
}
void AMyAGRCharacter::InitializeAttributes()
{
if (AbilitySystemComponent && DefaultAttributeEffect)
{
FGameplayEffectContextHandle EffectContext = AbilitySystemComponent->MakeEffectContext();
EffectContext.AddSourceObject(this);
FGameplayEffectSpecHandle SpecHandle = AbilitySystemComponent->MakeOutgoingSpec(DefaultAttributeEffect, 1, EffectContext);
if (SpecHandle.IsValid())
FActiveGameplayEffectHandle GEHandle = AbilitySystemComponent->ApplyGameplayEffectSpecToSelf(*SpecHandle.Data.Get());
}
}
void AMyAGRCharacter::GiveDefaultAbilities()
{
if (HasAuthority() && AbilitySystemComponent)
for (TSubclassOf<UGameplayAbility>& StartupAbility : DefaultAbilities)
AbilitySystemComponent->GiveAbility(FGameplayAbilitySpec(StartupAbility.GetDefaultObject(), 1, 0));
}
Tym sposobem implementacja GAS w cpp została ukończona, cała reszta odbywa się w blueprint’ach.
4.2 Advanced Game Ready PRO (AGR PRO)
Drugim frameworkiem na który się zdecydowałem to AGR PRO. Szereg komponentów, zmiennych i funkcji które ułatwiają ogólną pracę w projekcie. Wykorzystałem 4 komponenty: Animation component, Combat manager component, Equipment component oraz Inventory component.
