무민은귀여워

언리얼 엔진 샘플 게임 - 전략 게임 ( Strategy Game) 본문

IT/Unreal Engine 4

언리얼 엔진 샘플 게임 - 전략 게임 ( Strategy Game)

moomini 2019. 9. 10. 02:19
반응형

언리얼 샘플 게임 중 타워 디펜스 게임인 전략 게임을 읽어본다.

( Tower Defense 샘플은 RTS/타워 디펜스 게임 예제이다.)

게임 플레이 영상

https://www.youtube.com/watch?v=Uc_JgSNj3Vc

 

아래 문서 페이지를 참조해서 내용을 추가하고자 한다.

https://docs.unrealengine.com/ko/Resources/SampleGames/StrategyGame/index.html

 

전략 게임

타워 디펜스 게임에 대해 다루는 문서입니다.

docs.unrealengine.com

프로젝트 다운

프로젝트는 에픽 런처의 학습 탭에서 [전략 게임] 을 받으면 된다.

게임 구조 

AI 로직과 자동화된 폰

  Tower Defense 의 AI 로직은 간단한 유한 상태 머신(FSM) 구현입니다. 적 기지를 향해 이동하거나, 적을 공격하거나 하는 두 가지 상태 모두 StrategyAIAction 에서 상속된 별개의 클래스입니다.

 

  이 상태는 가장 중요한 동작이 처음에 오는 우선권 배열에 저장됩니다. 이 배열을 대상으로 반복처리하여 가장 적합한 동작이 선택되고, 우선권이 보다 높은 동작이 있는 경우 현재 동작을 중지합니다.

 

전략 게임 AI 클래스 구조

아래는 StrategyAIAction 클래스 헤더이다. 

 

현재 상태를 확인하고, 상태 전환을 해도 되는지 확인하는 등의 함수가 있다.

실제 공격이나 움직임을 구현하는 StrategyAIAction_AttackTarget 과 StrategyAIAction_MoveToBrewery 는 모두 이 클래스를 상속받는다. 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved.
#pragma once
 
 
#include "StrategyAIAction.generated.h"
 
class AStrategyAIController;
 
UCLASS(abstract, BlueprintType)
class UStrategyAIAction : public UObject
{
    GENERATED_UCLASS_BODY()
 
    /** 
     * Update this action.
     * @return false to finish this action, true to continue
     */
    virtual bool Tick(float DeltaTime);
 
    /** Activate action. */
    virtual void Activate();
 
    /** Should we activate action this time ? */
    virtual bool ShouldActivate() const;
 
    /** Abort action to start something else. */
    virtual void Abort();
 
    /** Can we abort this action? */
    virtual bool IsSafeToAbort() const;
 
    /** Set owning AI controller. */
    void SetController(AStrategyAIController* InOwner);
 
protected:
    /** Weak pointer to AI controller, to have faster access (cached information). */
    TWeakObjectPtr<AStrategyAIController>    MyAIController;
 
    /**    Tells us if we are already executed. */
    bool bIsExecuted;
};
cs

 

아래는 StrategyAIAction을 상속받은 StrategyAIAction_AttackTarget 헤더이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved.
 
#pragma once
 
#include "StrategyTypes.h"
#include "StrategyAIAction.h"
#include "StrategyAIAction_AttackTarget.generated.h"
 
UCLASS()
class UStrategyAIAction_AttackTarget : public UStrategyAIAction
{
    GENERATED_UCLASS_BODY()
 
public:
 
    // Begin StrategyAIAction interface
 
    /** Update in time current action */
    virtual bool Tick(float DeltaTime) override;
 
    /** activate action */
    virtual void Activate() override;
 
    /** abort action to start something else */
    virtual void Abort() override;
 
    /** can we abort this action? */
    virtual bool IsSafeToAbort() const override;
 
    /** Should we activate action this time ? */
    virtual bool ShouldActivate() const override;
 
    // End StrategyAIAction interface
 
protected:
    /** Pawn has hit something */
    void NotifyBump(FHitResult const& Hit);
 
    /** notify about completing current move */
    void OnMoveCompleted();
 
    /** move closer to target */
    void MoveCloser();
 
    /** updates any information about target, his location, target changes in ai controller, etc. */
    void UpdateTargetInformation();
 
    /** target actor to attack */
    TWeakObjectPtr<AActor> TargetActor;
 
    /** destination to move closer */
    FVector    TargetDestination;
 
    /** time when we will finish playing melee animation */
    float MeleeAttackAnimationEndTime;
 
    /** if pawn is playing attack animation */
    uint32 bIsPlayingAnimation : 1;
 
    /** set to true when we are moving to our target */
    uint32 bMovingToTarget : 1;
};
cs

 

 

  적군과 아군 폰 모두 같은 AI 로직으로 움직입니다. 적군 기지를 향해 이동하다가 다른 팀의 폰을 만나면 공격하는 것이죠. 플레이어가 아군 폰의 이동이나 행위를 제어할 수는 없지만, 새 유닛을 구매하여 스폰시킬 수는 있습니다.

 

  미니언 폰의 로직 추가에 블루프린트 가 사용되기도 했습니다. 적군과 아군 폰 모두 방패를 장착할 수 있는데, 아군 폰은 양조장에서 대장간 업그레이드를 구매하면 방패를 차게 되고, 적군 폰은 레벨 블루프린트 에서 호출되는 SpawnHeavyFunction 또는 SpawnEndBossFunction 를 통해 스폰되는 경우 방패를 차게 됩니다. 폰이 방패를 들게 되면 자동석궁의 화살이 막혀 피해를 입히지 못하게 됩니다. 이 로직은 블루프린트 인터페이스 를 사용하여 수행됩니다. Minion_Test 블루프린트 에도 충전된 화염 터렛에 맞은 적 폰을 늦추는 망이 들어 있습니다.

 

레벨 블루프린트 내의 SpawnHeavyFunction

건물 건설

  Tower Defense 에는 빌딩 클래스가 둘 있습니다. StrategyBuilding  StrategyBuilding_Brewery 인데요. Tower Defense 의 모든 터렛 유형은 물론이고 빈 건물 슬롯까지 StrategyBuilding 을 부모로 하여 블루프린트 로 디자인되어 있습니다. 플레이어가 빈 건물 슬롯을 클릭하면 컨텍스트 메뉴가 표시되며, 새 건물을 선택하면 건설이 시작됩니다. 건물이 건설되면 빈 건물 슬롯은 소멸되고 새 건물이 스폰됩니다.

 

  건물 업그레이드를 위한 방편도 마련되어 있습니다. StrategyBuilding_Brewery 클래스에 이러한 경우가 구현되어 있어, 업그레이드가 양조장 기지 근처 연결된 슬롯으로 내장됩니다.

 

  다시금 Tower Defense 의 코드는 기본 빌딩 클래스를 만들 뿐, Tower Defense 내 건물의 모든 로직과 디자인은 블루프린트 의 레벨 디자이너가 만든 것입니다.

 

↓소스에서 클래스 정의를 미리 해 둔 뒤, 블루프린트 클래스 내에서 설정을 한다는 것 같다.

예를 들어, 아래 소스에서 정의된 Cost, AdditionalCost, BuildTime 등의 변수들은 건물 블루 프린트 클래스의 디테일 탭에서 볼 수 있고 설정 또한 가능하다.

 

StrategyBuilding 헤더소스

 

건물 (빈 슬롯) 의 블루프린트

양조장

아군 양조장

  Brewery 블루프린트 에는 StrategyBuilding_Brewery 부모 클래스가 있으며, AIDirector 컴포넌트도 포함되어 있습니다. TowerDefenseMap 에는 두 개의 양조장이 배치되어 있는데, 하나는 적군 폰이 스폰되는 적 기지이고, 하나는 대장간과 무기고 업그레이드를 만들고 아군 폰을 스폰시킬 수 있는 아군 양조장입니다. Brewery 블루프린트 에는 그래프 로직이 없으며, 빌딩 프로퍼티에 대한 디폴트 세트와 AIDirector, TriggerBox, 스태틱 메시 가 포함된 컴포넌트 리스트 뿐입니다.

 

StrategyBuilding_Brewery 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved.
 
#pragma once
 
#include "StrategyBuilding.h"
#include "StrategyBuilding_Brewery.generated.h"
 
class AStrategyChar;
class UStrategyAIDirector;
class AStrategyBuilding;
 
 
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FWaveSpawnedDelegate);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FGameFinishedDelegate);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FConstructedUpgradeDelegate, AStrategyBuilding*, ConstructedBuilding);
 
 
UCLASS(Abstract, Blueprintable)
class AStrategyBuilding_Brewery : public AStrategyBuilding
{
    GENERATED_UCLASS_BODY()
 
private:
    /** team's AI director */
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Brewery, meta = (AllowPrivateAccess = "true"))
    UStrategyAIDirector* AIDirector;
public:
 
    /** The class of minion to spawn. */
    UPROPERTY(EditDefaultsOnly, Category=Brewery)
    TSubclassOf<AStrategyChar> MinionCharClass;
 
    /** left slot for upgrades to place in */
    UPROPERTY(EditInstanceOnly, Category=Brewery)
    TWeakObjectPtr<AStrategyBuilding>    LeftSlot;
 
    /** right slot for upgrades to place in */
    UPROPERTY(EditInstanceOnly, Category=Brewery)
    TWeakObjectPtr<AStrategyBuilding>    RightSlot;
 
    /** cost of each requested spawn */
    UPROPERTY(EditDefaultsOnly, Category=GameConfig)
    int32    SpawnCost;
 
    /** initial resources on easy difficulty */
    UPROPERTY(EditDefaultsOnly, Category=GameConfig)
    int32 ResourceInitialEasy;
 
    /** initial resources on medium difficulty */
    UPROPERTY(EditDefaultsOnly, Category=GameConfig)
    int32 ResourceInitialMedium;
 
    /** initial resources on hard difficulty */
    UPROPERTY(EditDefaultsOnly, Category=GameConfig)
    int32 ResourceInitialHard;
 
    /** delegate to broadcast about finished wave */
    UPROPERTY(BlueprintAssignable)
    FWaveSpawnedDelegate OnWaveSpawned;
 
    /** Event delegate for building upgrade construction complete. */
    UPROPERTY(BlueprintAssignable)
    FConstructedUpgradeDelegate OnConstructedUpgrade;
 
    /** class for empty slot */
    UPROPERTY()
    TSubclassOf<AStrategyBuilding> EmptySlotClass;
 
    /** get current number of lives */
    UFUNCTION(BlueprintCallable, Category=Brewery)
    uint8 GetNumberOfLives() const;
 
    /** set current number of lives*/
    UFUNCTION(BlueprintCallable, Category=Brewery)
    void SetNumberOfLives(uint8 NumberOfLives);
 
    /** event about constructed upgrade */
    virtual void OnConstructedBuilding(AStrategyBuilding* ConstructedUpgrade);
 
    // Begin Actor interface
    /** initial setup */
    virtual void PostInitializeComponents() override;
    // End Actor interface
 
 
    // Begin StrategyBuilding interface
 
    /** change current team */
    virtual void SetTeamNum(uint8 NewTeamNum) override;
 
    /** replace building with other class. return true if this building should never be built again */
    virtual bool ReplaceBuilding(TSubclassOf<AStrategyBuilding> NewBuildingClass) override;
 
    /** add additional button for spawning dwarfs here*/
    virtual void ShowActionMenu() override;
 
    // End StrategyBuilding interface
 
    /** spawns a dwarf */
    bool SpawnDwarf();
 
    /** gets spawn queue length string */
    FText GetSpawnQueueLength() const;
 
    /** notify about new game state */
    void OnGameplayStateChange(EGameplayState::Type NewState);
 
protected:
    /** Number of lives. */
    uint8    NumberOfLives;
 
public:
    /** Returns AIDirector subobject **/
    FORCEINLINE UStrategyAIDirector* GetAIDirector() const { return AIDirector; }
};
cs

 

 

양조장

 

업그레이드

  아군 양조장에는 업그레이드 슬롯이 두 개 붙어있습니다. 이 슬롯은 역시 StrategyBuilding 클래스에서 파생된 블루프린트 클래스 입니다. Brewery 메뉴에서 업그레이드를 선택하면, 한 슬롯은 그 업그레이드로 대체됩니다. 대장간 및 무기고 업그레이드는 한 번만 구매할 수 있습니다.

 

  대장간 업그레이드가 구매되면 건설이 시작되고 Wall_Smithy 블루프린트 에서 OnBuildStarted 이벤트가 발동됩니다. 이 블루프린트 는 업그레이드 건설이 완료되면 시스템에 업그레이드 건설이 완료되었음을 알리기도 합니다. 이 시점 이후부터 아군 폰에는 방패가 장착되어 스폰되기 시작하며, 이 방패 어태치먼트는 StrategyAttachment 클래스로부터 파생된 블루프린트 입니다. 대장간 건설 완료 보고 이후 "방패 부착" 행위가 할당되는 망은 TowerDefenseMap 레벨 블루프린트 의 접힌 그래프 PlayerBaseUpgrades 에 존재합니다. StrategyAttachment 클래스에는 단순히 SkeletalMeshComponent 가 들어있으며, 메시와 어태치먼트 부착 지점은 Attachment_Armorer 블루프린트  디폴트 에 설정됩니다.

 

↓건설 시작

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
bool AStrategyBuilding::StartBuild()
{
    if (bIsContructionFinished)
    {
        return false;
    }
 
    if (!bIsBeingBuild)
    {
        bIsBeingBuild = true;
 
        Health = 1;
        InitialBuildTime = RemainingBuildTime = GetBuildTime();
        OnBuildStarted();
 
        SetActorTickEnabled(true);
        if (ConstructionStartStinger)
        {
            UGameplayStatics::PlaySoundAtLocation(this, ConstructionStartStinger, GetActorLocation());
        }
        return true;
    }
 
    return false;
}
cs

 

OnBuildStarted 이벤트가 발동

Wall_Smithy   블루프린트

 

레벨블루프린트 - PlayerBaseUpgrades  

 

  무기고 블루프린트 에는 OnBuildStarted  OnBuildFinished 이벤트와 같은 로직 셋업이 들어 있습니다. 무기고 건설 이후 스폰되는 아군 폰은 망치가 장착되며, 역시 StrategyAttachment 클래스에서 파생됩니다. 대장간 건설이 완료되었다는 보고 이후 "망치 부착" 동작을 할당하는 망은 TowerDefenseMap 레벨 블루프린트 의 접힌 그래프 PlayerBaseUpgrades 에도 존재합니다.

 

터렛

빈 슬롯

빈 슬롯 클릭시 선택 메뉴

  빈 슬롯 역시 StrategyBuilding 을 부모 클래스로 하는 블루프린트 Wall_EmptySlot 입니다. 이 블루프린트 그래프에는 로직이 없으며, 이는 단지 블루프린트 클래스 로, 빌딩 프로퍼티와 스태틱 메시에 대해 디폴트 가 설정되어 있고, 트리거 박스가 컴포넌트 로 설정된 것입니다.

 

  가능한 모든 터렛 업그레이드는 빌딩 카테고리의 업그레이드 섹션 내 Wall_EmptySlot 블루프린트  디폴트 에 설정되어 있습니다.

석궁

석궁

  Wall_arbalest 블루프린트 에는 기본 터렛 유형인 석궁의 로직이 들어 있습니다. 석궁은 가장 가까운 적에게 중간 세기의 화살을 발사하며, 기본 모드는 화살 자동 발사입니다. 플레이어는 수동으로 석궁을 발사할 수도 있는데, 클릭한 후 화살을 발사하고자 하는 방향으로 끌어 놓으면 됩니다. 마우스를 길게 끌어 놓을 수록 더욱 강력하게 발사됩니다.

 

Wall_arbalest  

 

  석궁 화살은 다른 블루프린트 Projectile_arbalest 에 저장되며, StrategyProjectile 를 갖는 TestProjectile 블루프린트 에서 파생된 것입니다. Wall_arbalest 블루프린트 에는 다수의 하위망이 있으며, 모두 이벤트 그래프 안에 들어 있습니다. 컨스트럭션 스크립트 에는 블루프린트 로직이 없습니다.

 

Projectile_arbalest  

자동 석궁

자동석궁

  Wall_arbalest_auto 블루프린트 에는 자동석궁 로직이 들어 있습니다. 자동석궁은 벽에서 일직선으로 화살을 발사하며, 화살이 관통하는 모든 유닛에게 약간씩의 피해를 입힙니다. 자동석궁 화살은 벽이나 방패를 든 적에 닿을 때까지 소멸되지 않습니다. 자동석궁을 클릭한 다음 원하는 방향으로 끌어 놓는 방식으로 조준할 수 있습니다. 그러면 자동석궁은 마우스 버튼이 눌려 있는 동안 조준한 방향으로 계속해서 발사되며, 버튼을 놓으면 기본 발사 위치로 되돌아갑니다.

 

  석궁과 마찬가지로 이 터렛은 별도의 블루프린트 에 들어있는 화살을 발사합니다. Projectile_arbalest_auto 자동석궁 화살은 방패를 든 적이나 벽에 맞을 때까지 사라지지 않으며, 이러한 동작은 Interface_Auto_Arbalest  Interface_Auto_Projectile 블루프린트 인터페이스 의 도움으로 이루어집니다.

화염방사기 

화염방사기

화염방사기는 다른 터렛 유형과 같은 화살을 발사하지는 않습니다. 그 대신 화염 영역 내 모든 적을 불태웁니다. 플레이어가 화염방사기를 클릭한 뒤 누르고 있는 것으로 충전시키는 것이 가능하며, 마우스 버튼을 얼마나 오래 누르고 있었는가에 따라 충전된 화염은 최대 세 배 까지의 피해를 입힐 수 있으며, 충전된 화염에 맞은 적 폰을 늦추는 것도 가능합니다. 플레이어가 화염방사기를 충전시키면, 약간의 재사용 대기시간 경과 후 보통의 화염이 계속해서 방사됩니다.

카메라

  Tower Defense 의 카메라에는 시야각이 고정되어 있으며, 마우스 스크롤 휠 버튼으로 줌 인/아웃이 가능합니다. 카메라 계산은 StrategyPlayerController 클래스 내 CalcCamera 함수 내에서 이루어지는 반면, 카메라 최소/최대 오프셋, 카메라 각도, 카메라 속도 같은 상수를 DefaultGame.ini 에서 설정할 수 있습니다.

 

↓ StrategyPlayerController 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved.
 
#pragma once
 
#include "StrategyTeamInterface.h"
#include "StrategyPlayerController.generated.h"
 
class AStrategySpectatorPawn;
class UStrategyCameraComponent;
    
UCLASS()
class AStrategyPlayerController : public APlayerController, public IStrategyTeamInterface
{
    GENERATED_UCLASS_BODY()
 
public:
    // Begin PlayerController interface
    /** fixed rotation */
    virtual void UpdateRotation(float DeltaTime) override;
 
protected:
    /** update input detection */
    virtual void ProcessPlayerInput(const float DeltaTime, const bool bGamePaused) override;
    virtual void SetupInputComponent() override;
    // End PlayerController interface
 
public:
 
    // Begin StrategyTeamInterface interface
    virtual uint8 GetTeamNum() const override;
    // End StrategyTeamInterface interface
    
    /** set desired camera position. */
    void SetCameraTarget(const FVector& CameraTarget);
 
    /** helper function to toggle input detection. */
    void SetIgnoreInput(bool bIgnore);
 
    /** Input handlers. */
    void OnTapPressed(const FVector2D& ScreenPosition, float DownTime);
    void OnHoldPressed(const FVector2D& ScreenPosition, float DownTime);
    void OnHoldReleased(const FVector2D& ScreenPosition, float DownTime);
    void OnSwipeStarted(const FVector2D& AnchorPosition, float DownTime);
    void OnSwipeUpdate(const FVector2D& ScreenPosition, float DownTime);
    void OnSwipeReleased(const FVector2D& ScreenPosition, float DownTime);
    void OnSwipeTwoPointsStarted(const FVector2D& ScreenPosition1, const FVector2D& ScreenPosition2, float DownTime);
    void OnSwipeTwoPointsUpdate(const FVector2D& ScreenPosition1, const FVector2D& ScreenPosition2, float DownTime);
    void OnPinchStarted(const FVector2D& AnchorPosition1, const FVector2D& AnchorPosition2, float DownTime);
    void OnPinchUpdate(const FVector2D& ScreenPosition1, const FVector2D& ScreenPosition2, float DownTime);
 
    /** Toggles the ingame menu display. */
    void OnToggleInGameMenu();
 
    /** Handler for mouse leaving the minimap. */
    void MouseLeftMinimap();
 
    /** Handler for mouse pressed over minimap. */
    void MousePressedOverMinimap();
    
    /** Handler for mouse release over minimap. */
    void MouseReleasedOverMinimap();
 
protected:
    /** if set, input and camera updates will be ignored */
    uint8 bIgnoreInput : 1;
 
    /** currently selected actor */
    TWeakObjectPtr<AActor> SelectedActor;
 
    /** Swipe anchor. */
    FVector SwipeAnchor3D;
 
    FVector2D PrevSwipeScreenPosition;
 
    /** Previous swipe mid point. */
    FVector2D PrevSwipeMidPoint;
 
    /** Custom input handler. */
    UPROPERTY()
    class UStrategyInput* InputHandler;
 
    /** 
     * Change current selection (on toggle on the same). 
     *
     * @param    NewFocus    Actor to focus on.
     * @param    NewPosition    
     */
    void SetSelectedActor(AActor* NewFocus, const FVector& NewPosition);
 
    /** 
     * Get friendly target under screen space coordinates.
     *
     * @param    ScreenPoint    Screen coordinates to check
     * @param    WorldPoint    Point in the world the screen coordinates projected onto.
     */
    AActor* GetFriendlyTarget(const FVector2D& ScreenPoint, FVector& WorldPoint) const;
 
    /** 
     * Get audio listener position and orientation.
     * 
     * @param    
     * @param    
     * @param    
     */
    virtual void GetAudioListenerPosition(FVector& Location, FVector& FrontDir, FVector& RightDir) override;
 
private:
    /** Helper to return cast version of Spectator pawn. */        
    AStrategySpectatorPawn* GetStrategySpectatorPawn() const;
    
    /** Helper to return camera component via spectator pawn. */
    UStrategyCameraComponent* GetCameraComponent() const;
};
cs

 

↓ DefaultGame.ini

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[/Script/StrategyGame.StrategyGameMode]
TimeBeforeReturnToMenu=3
 
[/Script/StrategyGame.StrategyGameState]
WarmupTime=3
 
[/Script/StrategyGame.StrategyAISensingComponent]
SightDistance=300.0
 
[/Script/StrategyGame.StrategyCameraComponent]
MinCameraOffset=500
MaxCameraOffset=8000
FixedCameraAngle=(Pitch=-45,Yaw=-45,Roll=0)
CameraSpeed=3750
CameraActiveBorder=20
MinZoomLevel=0.4
MiniMapBoundsLimit=0.8
bShouldClampCamera=true
 
[/Script/EngineSettings.GeneralProjectSettings]
Description=an example for a strategy game
ProjectID=0D4E4DBE4EDDC35A00C9038387C616B6
ProjectName=Strategy Game
cs

 

  관람자 폰은 보이는 폰이 없는 플레이어를 만들 때 사용됩니다.

인게임 HUD

인게임 HUD 는 캔버스 드로잉과 슬레이트 위젯을 혼합시켜 만듭니다.

 

인게임 HUD

  좌상단 구석의 게임 타이머는 게임 워밍업 동안의 카운트 다운을 담당하며, SStrategySlateHUDWidget 클래스의 GetGameTime 함수를 사용합니다. 게임이 시작되면 이 카운트다운은 사라지며, 남은 생명 수(1)가 표시됩니다. "lives left" 표시에 대한 프로퍼티는 AStrategyHUD 클래스의 DrawLives 함수에 설정되어 있으며, 초기 생명 수는 TowerDefenseMap 레벨 블루프린트  PlayerBaseUpgrades 서브그래프에 설정됩니다.

 

↓ StrategyHUD 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved.
 
#pragma once
 
#include "StrategyHUD.generated.h"
 
UCLASS()
class AStrategyHUD : public AHUD
{
    GENERATED_UCLASS_BODY()
 
public:
 
    // Begin HUD interface
    virtual void DrawHUD() override;
    // End HUD interface
 
    /** Returns true if the "Pause" Menu up. */
    bool IsPauseMenuUp() const;
 
    /** 
     * Hides all the action buttons.
     * 
     * @param bInstantHide    Whether to hide immediately or defer
     */
    void HideAllActionButtons(bool bInstantHide=false);
 
    /** clears any pending actions (blinking button state) */
    void ClearActionRequiredStates();
 
    /** 
     * Gets single action button data.
     *
     * @param    Index    Index of the button to get the info for.
     */
    TSharedPtr<FActionButtonInfo> GetActionButton(int32 Index) const;
 
    /** gets transparent slate widget covering whole screen */
    TSharedPtr<class SStrategySlateHUDWidget> GetHUDWidget() const;
 
    /** sets actor for which action grid is displayed */
    void SetActionGridActor(AActor* SelectedActor);
 
    /** Toggles the in-game pause menu */
    void TogglePauseMenu();
 
    /** Enables the black screen, used for transition from game */
    void ShowBlackScreen();
 
    /** position to display action grid */
    FVector2D ActionGridPos;
 
    /** mini map margin */
    float MiniMapMargin;
 
    /** default action texture to use */
    UPROPERTY()
    UTexture2D* DefaultActionTexture;
 
    /** bigger, centered action default texture */
    UPROPERTY()
    UTexture2D* DefaultCenterActionTexture;
 
    /** minimap frustum points */
    FVector2D MiniMapPoints[4];
    
    /** current UI scale */
    float UIScale;
 
protected:
 
    /** draws mouse pointer */
    void DrawMousePointer();
 
    /** draws mini map */
    void DrawMiniMap();
 
    /** builds the slate widgets */
    void BuildMenuWidgets();
 
    /** draw number of lives for player */
    void DrawLives() const;
 
    /** 
     * Draws health bar for specific actor.
     *
     * @param    ForActor    Actor for which the health bar is for.
     * @param    HealthPct    Current Health percentage.
     * @param    BarHeight    Height of the health bar
     * @param    OffsetY        Y Offset of the health bar.
     */
    void DrawHealthBar(AActor* ForActor, float HealthPct, int32 BarHeight, int OffsetY = 0const;
 
    /** draw health bars for actors */
    void DrawActorsHealth();
 
    /** gets position to display action grid */
    FVector2D GetActionsWidgetPos() const;
 
    /** gets player controller */
    class AStrategyPlayerController* GetPlayerController() const;
 
    /** HUD menu widget */
    TSharedPtr<class SStrategySlateHUDWidget> MyHUDMenuWidget;
 
    /** actor for which action grid is displayed*/
    TWeakObjectPtr<AActor> SelectedActor;
 
    /** gray health bar texture */
    UPROPERTY()
    class UTexture2D* BarFillTexture;
 
    /** player team health bar texture */
    UPROPERTY()
    class UTexture2D* PlayerTeamHPTexture;
 
    /** enemy team health bar texture */
    UPROPERTY()
    class UTexture2D* EnemyTeamHPTexture;
 
    /** mouse pointer material (default) */
    UPROPERTY()
    class UMaterial* MousePointerNeutral;
 
    /** mouse pointer material (attack) */
    UPROPERTY()
    class UMaterial* MousePointerAttack;
 
    /** Pause button texture */
    UPROPERTY()
    UTexture2D* ActionPauseTexture;
 
    /** menu button texture */
    UPROPERTY()
    UTexture2D* MenuButtonTexture;
 
    /** resource texture - gold coin */
    UPROPERTY()
    UTexture2D* ResourceTexture;
 
    /** lives texture - barrel */
    UPROPERTY()
    UTexture2D* LivesTexture;
 
    /** if we are currently drawing black screen */
    uint8 bBlackScreenActive : 1;
};
cs

 

 

  현재 금 자원은 화면 상단 중간에 표시됩니다(2). 게임 타이머와 자원 표시 모두 SStrategySlateHUDWiget 안에 기본 위젯을 사용하여 정의되었습니다. 최상위 레벨 위젯 전부가 이 클래스를 사용하여 생성되었으나, 이 위젯 전부가 기본적으로 표시되는 것은 아닙니다.

 

UI 클래스 구조

 

  미니맵은 HUD 좌하단 구석에 있습니다(3). 캔버스를 사용해서 그려지는 실제 맵 이미지와 입력을 처리하는 투명 슬레이트 위젯 오버레이로부터 만들어진 것입니다. 미니맵 영역에 버튼을 클릭한 뒤 드래그할 때의 카메라 이동은 SStrategyMiniMapWidget 가 담당합니다.

 

  건물 슬롯에 클릭하면 SStrategyActionGrid 메뉴가 나타납니다. 이 위젯의 인스턴스는 하나밖에 없으며, 그 위치는 활성화된 건물 슬롯에 의해 결정됩니다. 메뉴의 화면 위치 계산은 DrawHUD 메서드 안에서 이루어지며, 여기서 선택된 액터 위치를 2D 좌표로 투영해 줍니다. 이 메뉴에 대한 액션 버튼의 이벤트 매핑과 모양은 AStrategyBuilding 클래스의 ShowActionMenu 메서드나 ShowCustomAction 메서드 중 하나에서 정의됩니다. Button 위젯은 SStrategyButtonWidget 클래스에서 정의되며, 액션 버튼에 바인딩된 부가 정보는 FActionButton 정보 구조체에 저장됩니다.

 

  폰과 건물의 체력 막대는 DrawActorsHealth 메서드를 사용해서 캔버스 위에 그려집니다. 각 팀마다 다른 체력 막대 텍스처가 있습니다.

 

  HUD 우하단 구석에는 PauseButton (4)으로, 게임 일시정지와 인게임 메뉴 표시여부를 토글시키는 버튼이 있습니다.

 

  게임 시간이 다 되거나 기지 중 하나가 파괴되면, 게임은 일시정지되고 "Victory" 또는 "Defeat" 텍스트 애니메이션이 화면 중앙에 표시됩니다. 애니메이션의 폰트 크기는 시간의 흐름에 따라 변합니다. 이 텍스트는 Visibility, Font, Text 에 대한 델리게이트가 포함된 간단한 STextBlock 위젯을 사용해서 만들어진 것입니다.

메뉴

메인 메뉴

  메인 메뉴는 메뉴 전용 HUD 를 로드하는 StrategyMenu 맵에 포함되어 있습니다. 메뉴는 슬레이트 기반으로, SStrategyMenuWidget 이 메인 메뉴 애니메이션, 레이아웃, 이벤트 처리를 담당합니다. SStrategyMenuItem 클래스는 인게임 허드에서 사용된 SStrategyButtonWidget 를 상속하며, 단일 메뉴 항목에 대한 설명이 됩니다. 각 메뉴 항목과, 그 항목에 붙은 이벤트들은 StrategyMenuHUD 에 정의됩니다.

 

  이전 메뉴로 돌아가기 위해 메뉴들에 대한 공유 포인터 배열이 MenuHistory 변수에 저장됩니다. 이 변수는 전에 확인한 메뉴를 저장하는 스택같은 역할을 하여, 되돌아가기가 쉬우면서도 메뉴를 여러 곳에서 재사용할 수 있도록 한 메뉴의 부모를 저장해야 하는 요건을 없애 주기도 합니다.

 

MenuHistory  선언

 

MenuHistory  사용예

  메뉴 애니메이션에서는 SStrategyMenuWidget::SetupAnimations 에 정의된 보간 커브를 사용합니다. 각 커브에는 시작 시간, 기간, 보간 방법이 정의되어 있으며, 정방향으로도 역방향으로도 재생 가능합니다. 특정 시간에서의 특성 애니메이션을 위해 0.0f 에서 1.0f 사이의 값이 반환되는 GetLerp() 가 사용됩니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void SStrategyMenuWidget::SetupAnimations()
{
    const float StartDelay = 0.0f;
    const float SecondDelay = 0.3f;
    const float AnimDuration = 0.5f;
    const float MenuChangeDuration = 0.2f;
    MenuWidgetAnimation = FCurveSequence();
    LeftMenuWidgetAnimation = FCurveSequence();
    LeftMenuScrollOutCurve = LeftMenuWidgetAnimation.AddCurve(0,MenuChangeDuration,ECurveEaseFunction::QuadInOut);
    LeftMenuWidgetAnimation.Play(this->AsShared());
 
    //Define the fade in animation
    TopColorCurve = MenuWidgetAnimation.AddCurve(StartDelay, AnimDuration, ECurveEaseFunction::QuadInOut);
 
    //now, we want these to animate some time later
 
    //rolling out
    BottomScaleYCurve = MenuWidgetAnimation.AddCurve(StartDelay+SecondDelay, AnimDuration, ECurveEaseFunction::QuadInOut);
    //fading in
    BottomColorCurve = MenuWidgetAnimation.AddCurve(StartDelay+SecondDelay, AnimDuration, ECurveEaseFunction::QuadInOut);
    //moving from left side off screen
    ButtonsPosXCurve = MenuWidgetAnimation.AddCurve(StartDelay+SecondDelay, AnimDuration, ECurveEaseFunction::QuadInOut);
 
    FSlateApplication::Get().PlaySound(MenuStyle->MenuEnterSound);
 
}
cs

 

인게임 메뉴

인게임 메뉴가 활성화되면 반투명 전체화면 슬레이트 오버레이가 표시되며, 게임은 일시정지됩니다. PauseMenuButtons  SStrategySlateHUDWidget 에 정의됩니다. 인게임 일시정지 메뉴에는 버튼이 둘 있습니다. 하나는 게임 종료, 다른 하나는 메인 메뉴 복귀입니다. 인게임 메뉴를 빠져나가려면 우하단 구석의 일시정지 버튼을 한 번 더 눌러야 합니다.

레벨 블루프린트

레벨 블루프린트 에는 각 웨이브 스폰은 물론 초기화와 승리/패배 조건 등이 결정되는 모듈식 구조체가 있습니다.

적 스폰

  각 웨이브는 spawn fast, span normal, span heavy 의 세 가지 블루프린트 매크로 를 사용해서 구성됩니다. 이들 각각은 특정 유닛 파라미터와 어태치먼트를 구성한 다음 StrategyAIDirector  SpawnMinions 함수 발동을 기다립니다. 이 매크로는 적 양조장의 StrategyAIDirector 의 웨이브 스폰 완료 보고를 기다렸다가 실행이 하위망을 빠져나가도록 허용합니다.

 

 

  각 스폰 매크로는 두 개의 실행 입력과 하나의 정수 입력을 받는데, 실행 입력 하나는 매크로의 시작이고 다른 하나는 OnWaveSpawned 이벤트 발동 이후 Gate 를 여는 것이며, 정수 입력은 스폰시킬 폰 갯수입니다.
StrategyAIDirector 에서의 함수가 각 폰 웨이브 유형에 알맞는 입력과 함께 호출됩니다. 그 세 가지 함수는 SetDefaultWeapon, SetDefaultArmor, SetBuffModifier 입니다.
SetDefaultWeapon  SetDefaultArmor  블루프린트 를 입력으로 받으며, 그 블루프린트 를 새로 스폰되는 것의 기본 무기나 방어구로 할당시킵니다. 예를 들어 SpawnFastMacro 로 스폰되는 모든 적 스폰은 Attachment_Smithy 망치 블루프린트 가 기본 무기로 되어 있으며, SpawnHeavyMacro 로 스폰되는 모든 적 폰에는 Attachment_Armorer 방패 블루프린트 가 기본 방어구로 되어 있습니다.

 

  스폰중인 블루프린트 함수에서 호출되는 마지막 StrategyAIDirector 함수는 SetBuffModifier 로, 폰의 공격 능력, 체력 보너스, 속도, 크기와 같은 여러가지 데이터 입력을 갖고 있습니다. 이 입력은 모두 블루프린트 에 노출되어 있어서, 스폰시킬 적 폰의 클래스를 레벨 디자이너가 새로 만들기가 매우 쉽습니다. 최종적으로 각 스폰중인 블루프린트 함수는 적 양조장 StrategyAIDirector  WaveSize 프로퍼티를 설정합니다.

 

  적 웨이브의 갯수는 다섯으로, 각기 빠른/보통/무거운 적 폰 조합이 다양합니다. 웨이브 시작시 Show Wave Title 노드가 몇 번째 웨이브인지를 표시합니다. 그런 다음 웨이브의 첫 적 스폰이 호출됩니다. 스폰 이후에는 두 가지 유형의 딜레이가 있는데, Delay 노드로 설정되는 타이머 딜레이가 있고, WaitForWaveMacro 로 설정되는 폰 기반 딜레이가 있습니다. WaitForWaveMacro 매크로는 살아있는 적 폰 갯수를 수시로 확인하여 딜레이 시간이 만료되거나 모든 적 폰이 죽을 때까지 실행이 매크로에 머물도록 합니다. 한 웨이브의 모든 스폰이 끝나고, 해당 웨이브의 모든 적 폰이 죽(거나 2 분이 지났)으면 Remote Event 노드를 사용해서 다음 웨이브에 대한 Custom Event 를 실행시킵니다.

승리 및 패배 조건

  게임에서 기지의 생명은 세 개 입니다. 적 폰이 아군 양조장에 도달하면 생명이 하나 깎이고, 모든 생명이 깎이면 게임에 패배하게 됩니다. 적 보스가 아군 양조장에 도달해도 역시 게임에 패배하게 됩니다. 게임에 승리하기 위해서는 생명을 다 잃기 전까지 적 폰의 다섯 웨이브 모두를 무찔러야 합니다. 승리 및 패배 결정 조건을 구성하는 망은 TowerDefenseMap  레벨 블루프린트 안에 있으며, 함수 호출은 AGameModeBase 클래스에서 파생되는 StrategyGameInfo 클래스에 구성되어 있습니다. StrategyGameInfo 클래스에는 게임을 초기화키며 액터의 PreInitializeComponents, SetGamePaused, SetGameDifficulty 이전에 호출되기도 하는 InitGame 와 같은 함수도 들어 있습니다.

 

 

  웨이브 5 에 스폰되는 최종 보스를 죽이고 나면 Remote Event 노드를 통해 Winning Custom Event 가 호출됩니다. 그 후 Win Condition 코멘트 박스 안에 있는 이 Custom Event  WaitForWin 하위망 실행을 발동 및 트리거시켜 아직 살아있는 적 폰이 없는지 검사합니다. 살아있는 적이 없으면 Finish Game 함수에 WinningTeam 입력을 "Player" 로 설정하여 호출합니다.

 

  Lose Condition 코멘트 박스 안에는 플레이어가 게임에 패배했을 때 Finish Game 함수에 WinningTeam 입력을 "Enemy" 로 설정하여 호출하는 노드가 둘 있습니다. 첫 번째는 적 폰이 아군 양조장에 도달하여 생명 세 개를 모두 잃었을 때 트리거되는 것입니다. 적 폰이 아군 양조장에 도달할 때마다 MultiGate 가 트리거됩니다. MultiGate 노드의 첫째 둘째 출력 실행 핀 각각은, 트리거될 때마다 아군 양조장의 NumberOfLives 수치를 1 씩 감소시키는 노드에 연결됩니다. 후자쪽 출력 실행 핀은 아군 양조장의 생명 갯수를 0 으로 설정한 다음 Finish Game 함수에 WinningTeam 을 "Enemy" 로 설정하여 호출합니다. 적 보스가 스폰되면 "생명 세 개 짜리" MultiGate 로 통하는 Gate 노드를 닫고, WinningTeam 이 "Enemy" 로 설정된 두 번째 Finish Game 함수로 통하는 Gate 노드를 엽니다. 여기서 최종 보스가 아군 양조장에 도달하면 FinishGame 함수가 활성화되어 게임에 패배하는 망이 생성됩니다.

자원 노드 - 금

  금광은 StrategyResourceNode 를 부모 클래스로 갖는 블루프린트 입니다. 이 클래스에는 퍼블릭 함수 GetAvailableResources  GetInitialResources, 프로텍티드 함수 OnDepleted, 자원이 고갈되면 알리는 BlueprintImplementableEvent, 금광에 남은 자원 양을 설정하는 프로텍티드 프로퍼티 NumResources 가 들어 있습니다.

 

  금광 블루프린트 에는 타이머에 금광이 나타났다 사라졌다 하게 만드는 하위망이 들어 있습니다. 블루프린트  컨스트럭션 스크립트 는 금광이 레벨에 놓이면 자동으로 숨겨지도록 설정합니다. 금광이 나타나면 AppearFX 파티클 이펙트가 유령 노이즈와 함께 재생됩니다. 금광 채취에 성공하여 OnDepleted 이벤트가 발동되면, 금광에 존재하는 만큼의 금이 플레이어의 소지 금 총량에 더해집니다. CollectFX 파티클 이펙트와 CoinSound 가 재생되며, 노드는 다시 숨겨집니다. 플레이어가 노드에 클릭하여 제때 채취하는 데 실패하면 FadeFX 파티클 이펙트와 유령 사운드가 재생됩니다.

 

반응형
Comments