Unreal Engine 5 배워보기 - 21일차
컴퓨터 및 게임 관련 전공인이 아닌 문외한 일반인이 언리얼 엔진을 배우면서 정리한 내용입니다.
그날그날 배웠던 내용을 제가 나중에 보기 위해 정리한 것으로, 지금 당장 보기에 부족한 점이 아주아주 많고 추후에 수정이 될 수도 있습니다.
오탈자 및 잘못 기재된 내용 지적, 부족한 내용 설명은 언제든지 환영입니다.
본글은 언리얼 엔진 5.5.4 버전 영문판을 기준으로 합니다.
----------------------------------------------------------------------------------------------------------------
21일차 학습 내용
- 사격 소리 추가
- 아이템 습득
- 아이템 획득 후 장비 추가
- 아이템 교체
사격 소리 추가
소리 추가 코드를 예시로 하나만 작성해보도록 하겠다.
PlayerControl.h
1
2
|
UPROPERTY(EditAnywhere, Category = "Sound") // 새로 추가
USoundBase* HitSound;
|
cs |
*HitSound는 여기서 사용하는 변수가 아님; 나중에 사용할 예정이고 미리 작성만 해둠
PlayerControl.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
void APlayerCharacterControl::WeaponFireSound()
{
if (RifleSound)
{
UGameplayStatics::PlaySound2D(this, RifleSound);
// 람다 함수: 간단한 함수를 선언 없이 즉석해서 만드는 방식 / [캡쳐](매개변수){실행코드};
FTimerHandle HitSoundDelayHandle; // 타이머를 제어하기 위한 핸들 변수 (타이머 취소, 타이머 상태 확인) 선언
GetWorld()->GetTimerManager().SetTimer(HitSoundDelayHandle, // Get world() 현재 객체가 속한 월드를 가져옴 / GetTimerManager() 타이머를 관리하는 시스템 / SetTimer() 타이머를 설정하고 일정 시간이 지나면 특정 작업을 실행
[=]() { // [=] 모든 외부에 있는 변수 값을 사용함 / () 안에는 내가 원하는 매개변수 삽입가능
UGameplayStatics::PlaySound2D(this, HitSound);
},
1.0f, // 1초 뒤
false // 반복 x
);
}
}
|
cs |
이렇게 작성해줄 경우 플레이어 BP의 'RifleSound' 에서 지정한 소리를 1초 뒤에 반복없이 재생하게 된다.
위에서 사용된 함수는 "람다 함수" 라고 하는데, 함수를 선언하지 않고 즉석으로 선언하여 만드는 방식이다.
일회용으로 쓰는 함수는 위와 같은 방법으로 하기 좋으나, 이렇게 사용하는 함수가 많아질 경우 코드가 길어지고 너저분해지기에 주의해야한다.
"람다 함수"는 '[캡쳐 리스트] (매개변수 자리) -> {실행할 코드 블록};' 의 형태로 이뤄져있다.
위에서 작성한 대로 컴파일 할 경우, 위 사진과 같이 오류가 발생하는데, [=] 과 'this' 의 명령어는 c++20에서는 사용이 불가하다는 내용이다.
(버전에 호환이 되지 않는 명령어)
그렇기에 아래와 같이 바꿔주면 된다.
PlayerControl.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
void APlayerCharacterControl::WeaponFireSound()
{
if (RifleSound)
{
UGameplayStatics::PlaySound2D(this, RifleSound);
// 람다함수 간단한 함수를 선언 없이 즉석해서 만드는 방식 / [캡쳐](매개변수){실행코드};
FTimerHandle HitSoundDelayHandle; // 타이머를 제어하기 위한 핸들 변수 (타이머 취소, 타이머 상태 확인) 선언
GetWorld()->GetTimerManager().SetTimer(HitSoundDelayHandle, // Get world() 현재 객체가 속한 월드를 가져옴 / GetTimerManager() 타이머를 관리하는 시스템 / SetTimer() 타이머를 설정하고 일정 시간이 지나면 특정 작업을 실행
FTimerDelegate::CreateLambda([this]() { // FTimerDelegate::CreateLambda() 람다변수를 타이머에 연결 가능한 델리게이트로 변환 / [this] 현재 클래스의 멤버 변수 함수에 접근하기 위한 캡쳐 / () 안에는 내가 원하는 매개변수 삽입가능 / 오류 발생으로 코드 수정
UGameplayStatics::PlaySound2D(this, HitSound);
}),
1.0f, // 1초 뒤
false // 반복 x
);
}
}
|
cs |
이렇게 해줄 경우 소리가 정상적으로 작동하는 걸 확인할 수 있다.
이제 본격적으로 총 소리를 C++을 통해 삽입해보도록 하겠다.
우선은 C++에서 집어넣은 소리가 아닌, BP, Anim Montage 등에서 삽입된 소리는 모두 지워주고, 아래와 같이 코드를 작성해준다.
PlayerControl.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
void APlayerCharacterControl::Fire() // 사격 상태 애니메이션 연결
{
OnFire_BP(); // BP 동시실행 중요중요중요
UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
if (RifleFireMontage)
{
AnimInstance->Montage_Play(RifleFireMontage, 0.233333f);
}
FTimerHandle HitSoundDelayHandle; // 소리 재생 추가
GetWorld()->GetTimerManager().SetTimer(HitSoundDelayHandle, this,
&APlayerCharacterControl::WeaponFireSound,
0.2f,
false
);
}
|
cs |
그리고 위 사진과 같이 플레이어 BP에서 소리를 추가해주면 정상적으로 소리가 나는 걸 확인할 수 있다.
*'Hit Sound' 가 아닌 'Rifle Sound' 에서 넣어줘야함; 'Hit Sound' 는 아직 사용하는 것 아님
아이템 습득
아이템 습득 원리는 20일차에 배운 것과 크게 다를 것 없기에 달라진 점만 설명하고 나머지는 생략하도록 하겠다.
C++에서 습득한 아이템을 사라지게 할 코드를 작성해줄 것이기에 위 그림과 같이 "Destrot Actor" 노드의 연결을 끊어주고, 아래와 같이 코드를 입력해준다.
PlayerControl.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
void APlayerCharacterControl::Operate()
{
Operate_BP();
FVector StartLocation = GetMesh()->GetSocketLocation("hand_r");
FVector EndLocation = StartLocation;
float SphereRadius = 70.0f;
FHitResult HitResult;
FCollisionQueryParams Params;
Params.AddIgnoredActor(this); // 자기자신 무시
bool isHit = GetWorld()->SweepSingleByChannel(HitResult, StartLocation, EndLocation, FQuat::Identity, ECC_GameTraceChannel1, FCollisionShape::MakeSphere(SphereRadius), Params);
DrawDebugSphere(GetWorld(), StartLocation, SphereRadius, 12, FColor::Red, false, 1.0f); // Sphere Trace를 표현하는 디버깅
if (isHit)
{
FTimerHandle HitDelayHandle;
GetWorld()->GetTimerManager().SetTimer(HitDelayHandle,
FTimerDelegate::CreateLambda([HitResult]()
{
HitResult.GetActor()->Destroy();
}),
0.5f,
false
);
}
|
cs |
'hand_r' 소켓에서 '70' 크기의 "Sphere Trace" 를 생성하여, 해당 "Sphere Trace" 안에 아이템이 있을 경우 0.5초 뒤에 해당 아이템을 사라지게 하는 명령이다.
여기서 아이템이 사라지지 않는 경우가 있다.
위 11번째 줄의 코드를 보면 "GameTraceChannel1" 로 지정된 것을 확인할 수 있다.
"GameTraceChannel1" 로 지정된 "TraceChannel" 의 아이템과의 충돌설정이 'Ignore' 일 수도 있기 때문이다.
아이템 습득에 사용할 TraceChannel의 "Default Response" 을 'Ignore' 로 하여 새로 만든다.
아이템과의 상호작용만 할 TraceChannel이기에, 아이템 쪽만 'Block' 으로 바꿔주면 되기 때문이다.
그리고 프로젝트가 저장된 폴더로 가서 'Config' 폴더로 들어가면 위 사진처럼 파일들이 뜨는데, 위 파일들 중에 "DefaultGame.ini" 를 실행해 메모장을 켜준다.
메모장에서 'gametrace' 를 검색하면 위와 같이 우리가 생성한 TraceChannel들이 뜨는데, 그 중에서 방금 아이템과의 상호작용만을 위해 생성한 TraceChannel의 번호를 알아낸 뒤, "GameTraceChannel1" 을 해당 번호로 바꿔주면 된다.
여기까지만 할 경우 아이템이 근처에 없어도 "Sphere Trace" 가 생성되기에, 아이템이 캐릭터 앞에 있을 때에만 "Sphere Trace" 가 생성되도록 하고 싶을 경우, 아래와 같이 코드를 작성해주면된다.
PlayerControl.h
1
2
|
UPROPERTY(EditAnywhere, BlueprintReadWrite)
bool isItem = false;
|
cs |
PlayerControl.cpp
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
|
void APlayerCharacterControl::Operate()
{
if (!isItem) // 추가
{
return;
}
Operate_BP();
FVector StartLocation = GetMesh()->GetSocketLocation("hand_r");
FVector EndLocation = StartLocation;
float SphereRadius = 70.0f;
FHitResult HitResult;
FCollisionQueryParams Params;
Params.AddIgnoredActor(this); // 자기자신 무시
bool isHit = GetWorld()->SweepSingleByChannel(HitResult, StartLocation, EndLocation, FQuat::Identity, ECC_GameTraceChannel9, FCollisionShape::MakeSphere(SphereRadius), Params);
DrawDebugSphere(GetWorld(), StartLocation, SphereRadius, 12, FColor::Red, false, 1.0f); // Sphere Trace를 표현하는 디버깅
if (isHit)
{
FTimerHandle HitDelayHandle;
GetWorld()->GetTimerManager().SetTimer(HitDelayHandle,
FTimerDelegate::CreateLambda([HitResult]()
{
HitResult.GetActor()->Destroy();
}),
0.5f,
false
);
}
|
cs |
*이 코드의 경우 개인이 제작하고 싶은 게임에 따라 취사 선택 가능
아이템 획득 후 장비 추가
우선 플레이어가 장비할 아이템들을 액터 BP로 레벨에 배치한 뒤, 아이템과 플레이어와의 충돌 판정을 고려하여 "Collision Preset" 을 설정해준다.
아이템 습득을 bool 변수로 확인할 수도 있으나, 이렇게 할 경우 모든 아이템을 일일이 변수로 만들어줘야하기에 많이 번거롭다.
그렇기에 언리얼 엔진의 기능 중 하나인 "TMap" 으로 간편하게 설정해줄 수 있다.
플레이어가 습득할 수 있는 아이템의 목록을 Enum의 형태로 작성해주고, 작성해준 Enum과 일치하는 이름의 장비가 들어오면 장비를 착용할 수 있게 해주는 방식으로 코드를 짜주면 된다.
PlayerControl.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
UENUM() // 아이템 목록 Enum
enum class EWeaponType : uint8
{
Pistol UMETA(DisplayName = "Pistol"),
SMG UMETA(DisplayName = "SMG"),
Rifle UMETA(DisplayName = "Rifle"),
};
UPROPERTY(EditAnywhere, BlueprintReadWrite) // 아이템 확인 변수 추가
bool isItem = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite) // 아이템 목록 map 추가
TMap<EWeaponType, bool> IsWeaponMap;
|
cs |
PlayerControl.cpp
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
|
#include "Engine/DamageEvents.h" // 나중에 사용할 것인데 미리 추가
APlayerCharacterControl::APlayerCharacterControl()
{
IsWeaponMap.Add(EWeaponType::Pistol, false); // 아이템 소유 상태 확인
IsWeaponMap.Add(EWeaponType::SMG, false);
IsWeaponMap.Add(EWeaponType::Rifle, false);
}
void APlayerCharacterControl::Operate()
{
if (!isItem)
{
return;
}
Operate_BP();
FVector StartLocation = GetMesh()->GetSocketLocation("hand_r");
FVector EndLocation = StartLocation;
float SphereRadius = 70.0f;
FHitResult HitResult;
FCollisionQueryParams Params;
Params.AddIgnoredActor(this);
bool isHit = GetWorld()->SweepSingleByChannel(HitResult, StartLocation, EndLocation, FQuat::Identity, ECC_GameTraceChannel9, FCollisionShape::MakeSphere(SphereRadius), Params);
if (isHit) // 습득한 아이템과 map의 아이템 일치 여부 확인
{
if (HitResult.GetActor()->GetName()==TEXT("Pistol"))
{
IsWeaponMap[EWeaponType::Pistol] = true;
}
if (HitResult.GetActor()->GetName() == TEXT("SMG"))
{
IsWeaponMap[EWeaponType::SMG] = true;
}
if (HitResult.GetActor()->GetName() == TEXT("Rifle"))
{
IsWeaponMap[EWeaponType::Rifle] = true;
}
FTimerHandle HitDelayHandle;
GetWorld()->GetTimerManager().SetTimer(HitDelayHandle,
FTimerDelegate::CreateLambda([HitResult]()
{
HitResult.GetActor()->Destroy();
}),
0.5f,
false
);
}
/*UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
if (OperateMontage)
{
AnimInstance->Montage_Play(OperateMontage, 1.0f);
}*/
}
|
cs |
위에서 작성해준 Enum이 위 그림과 같이 플레이어 BP에 들어와있는 것을 확인할 수 있고, 해당 아이템과 일치하는 이름의 아이템을 얻게 되면 체크로 표시되면서 장비가 가능하게 된다.
이렇게 작성해줄 경우 문제가 발생한다.
위에서 입력한 습득 가능한 아이템의 이름과 레벨에 배치된 아이템의 이름이 일치하지 않는다는 것이다.
레벨에 배치된 아이템들은 위와 그림과 같이 이름이 뜬다.
그렇기에 아이템의 이름을 습득할 때 이름을 일치시켜주는 코드를 추가해줘야한다.
PlayerControl.cpp
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
|
void APlayerCharacterControl::Operate()
{
if (!isItem)
{
return;
}
Operate_BP();
FVector StartLocation = GetMesh()->GetSocketLocation("hand_r");
FVector EndLocation = StartLocation;
float SphereRadius = 70.0f;
FHitResult HitResult;
FCollisionQueryParams Params;
Params.AddIgnoredActor(this); // 자기자신을 무시
bool isHit = GetWorld()->SweepSingleByChannel(HitResult, StartLocation, EndLocation, FQuat::Identity, ECC_GameTraceChannel9, FCollisionShape::MakeSphere(SphereRadius), Params);
/*DrawDebugSphere(GetWorld(), StartLocation, SphereRadius, 12, FColor::Red, false, 1.0f);*/
if (isHit) // 습득한 아이템과 map의 아이템 일치 여부 확인
{
FString name = HitResult.GetActor()->GetName(); // 이름 앞뒤를 잘라줌
name = name.LeftChop(1);
name = name.LeftChop(name.Len()-name.Find(TEXT("_C_")));
/*if (name.Find(TEXT("BP_")) == 0)
{
name = name.RightChop(3);
}*/
/*name = name.RightChop(name.Len()-name.Find(TEXT("BP_")));
name = name.RightChop(3);*/
/*
* 만약, name에 BP_가 존재한다면, -> if
* 앞에서부터 3개를 잘라라 -> RightChop
*
*/
GEngine->AddOnScreenDebugMessage(-1, 10.0f, FColor::Red, FString::Printf(TEXT("%s"), *name));
if (name==TEXT("BP_Pistol")) // 이름 같은지 체크 / 이곳의 코드도 변경
{
IsWeaponMap[EWeaponType::Pistol] = true;
}
else if(name == TEXT("SMG"))
{
IsWeaponMap[EWeaponType::SMG] = true;
}
else if (name == TEXT("Rifle"))
{
IsWeaponMap[EWeaponType::Rifle] = true;
}
FTimerHandle HitDelayHandle;
GetWorld()->GetTimerManager().SetTimer(HitDelayHandle,
FTimerDelegate::CreateLambda([HitResult]()
{
HitResult.GetActor()->Destroy();
}),
0.5f,
false
);
}
/*UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
if (OperateMontage)
{
AnimInstance->Montage_Play(OperateMontage, 1.0f);
}*/
}
|
cs |
이렇게 하면 습득 시 아이템의 이름이 습득 가능한 아이템의 이름과 같아서 습득이 가능해지고, 위 사진과 같이 플레이어의 무기 카테고리에 체크 표시가 된 것을 확인할 수 있다.
아이템 교체
아이템을 습득할 수 있으니 이제 교체하는 기능을 넣을 수 있다.
항상 새 키에 기능을 바인딩 해줬듯이, IA를 만들고 IMC에 추가해주고, 아래와 같이 코드를 작성해준다.
PlayerControl.h
1
2
3
4
5
6
7
8
9
10
|
UPROPERTY(VisibleAnywhere, Category = "Input")
class UInputAction* WeaponChangeAction;
void RifleMode();
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Animation")
UAnimMontage* WeaponChangeMontage;
UPROPERTY(EditAnywhere, Category = "Sound")
USoundBase* WeaponChangeSound;
|
cs |
PlayerControl.cpp
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
|
static ConstructorHelpers::FObjectFinder<UInputAction>InputRifleChange(TEXT("/Script/EnhancedInput.InputAction'/Game/Input/IA_RifleChange.IA_RifleChange'")); // IA_WeaponChange지정
if (InputRifleChange.Object != nullptr)
{
WeaponChangeAction = InputRifleChange.Object;
}
void APlayerCharacterControl::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) // IMC 관련
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent);
EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &APlayerCharacterControl::Move);
EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &APlayerCharacterControl::Look);
EnhancedInputComponent->BindAction(AimAction, ETriggerEvent::Started, this, &APlayerCharacterControl::OnAim); // 조준 관련
EnhancedInputComponent->BindAction(AimAction, ETriggerEvent::Completed, this, &APlayerCharacterControl::OffAim);
EnhancedInputComponent->BindAction(FireAction, ETriggerEvent::Started, this, &APlayerCharacterControl::Fire); // 사격 관련
EnhancedInputComponent->BindAction(OperateAction, ETriggerEvent::Started, this, &APlayerCharacterControl::Operate); // 아이템 감지 관련
EnhancedInputComponent->BindAction(WeaponChangeAction, ETriggerEvent::Started, this, &APlayerCharacterControl::RifleMode); //무기 교체 관련 추가
}
void APlayerCharacterControl::RifleMode()
{
if (IsWeaponMap.Contains(EWeaponType::Rifle) && IsWeaponMap.Num() > 0 && IsWeaponMap[EWeaponType::Rifle] == true)
{
if (WeaponChangeMontage)
{
UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
AnimInstance->Montage_Play(WeaponChangeMontage, 1.733333f);
}
FTimerHandle HitDelayHandle;
GetWorld()->GetTimerManager().SetTimer(HitDelayHandle,
FTimerDelegate::CreateLambda([this]()
{
if (WeaponChangeSound)
{
UGameplayStatics::PlaySound2D(this, WeaponChangeSound);
}
}),
0.8f,
false
);
}
else // 총이 없는 경우
{
UGameplayStatics::PlaySound2D(this, WeaponChangeSound);
}
}
|
cs |
그러면 "Rifle" 이라는 장비를 습득하고 위에서 지정해준 RifleMode로 바꿔주는 키를 입력했을 경우, 지정된 애니메이션과 소리가 나면서 장비를 교체할 수 있게 된다.
*오늘까지 작성한 코드 정리
PlayerControl.h
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
|
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "InputActionValue.h"
#include "PlayerCharacterControl.generated.h"
UENUM() // 아이템 목록 Enum
enum class EWeaponType : uint8
{
Pistol UMETA(DisplayName = "Pistol"),
SMG UMETA(DisplayName = "SMG"),
Rifle UMETA(DisplayName = "Rifle"),
};
UCLASS()
class MYPROJECT_API APlayerCharacterControl : public ACharacter
{
GENERATED_BODY()
public:
// Sets default values for this character's properties
APlayerCharacterControl();
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
// Called to bind functionality to input
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
protected:
UPROPERTY(EditAnywhere, BlueprintReadWrite) // readwrite 추가
class USpringArmComponent* SpringArm;
UPROPERTY(EditAnywhere, BlueprintReadWrite) // 총알 발사 수정
class UCameraComponent* Camera;
private:
UPROPERTY(VisibleAnywhere, Category = "Input")
class UInputMappingContext* DefaultContext;
UPROPERTY(VisibleAnywhere, Category = "Input")
class UInputAction* MoveAction;
UPROPERTY(VisibleAnywhere, Category = "Input")
class UInputAction* LookAction;
UPROPERTY(VisibleAnywhere, Category = "Input")
class UInputAction* AimAction;
UPROPERTY(VisibleAnywhere, Category = "Input")
class UInputAction* FireAction;
UPROPERTY(VisibleAnywhere, Category = "Input")
class UInputAction* OperateAction;
UPROPERTY(VisibleAnywhere, Category = "Input") // 추가
class UInputAction* WeaponChangeAction;
UPROPERTY(VisibleAnywhere, Category = "Input")
float mousespeed = 30.0f;
void Move(const FInputActionValue& value);
void Look(const FInputActionValue& value);
void OnAim();
void OffAim();
void Fire();
void Operate();
void RifleMode();
private:
UPROPERTY(EditAnywhere)
class UStaticMeshComponent* SM_Weapon;
public:
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Animation")
UAnimMontage* AimMontage;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Animation")
UAnimMontage* RifleFireMontage;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Animation")
UAnimMontage* OperateMontage;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Animation")
UAnimMontage* WeaponChangeMontage;
UPROPERTY(EditAnywhere, Category = "Sound")
USoundBase* RifleSound;
UPROPERTY(EditAnywhere, Category = "Sound")
USoundBase* HitSound; // 새로 추가
UPROPERTY(EditAnywhere, Category = "Sound")
USoundBase* WeaponChangeSound; // 새로 추가
void WeaponFireSound(); // 변수가 아니라 UPROPERTY 추가해주면 안됨
UPROPERTY(EditAnywhere, BlueprintReadWrite) // 아이템 확인 변수 추가
bool isItem = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite) // 아이템 목록 map 추가
TMap<EWeaponType, bool> IsWeaponMap;
UFUNCTION(BlueprintImplementableEvent, Category = "Input")
void OnFire_BP(); // 정의 없이 선언만 있음; public으로 해야함! 중요!
UFUNCTION(BlueprintImplementableEvent, Category = "Input") // 줌인 시 크로스헤어 추가 생성
void OnAim_BP();
UFUNCTION(BlueprintImplementableEvent, Category = "Input") // 줌아웃 시 크로스헤어 추가를 위해 생성
void OffAim_BP();
UFUNCTION(BlueprintImplementableEvent, Category = "Input")
void Operate_BP();
};
|
cs |
PlayerControl.cpp
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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
|
// Fill out your copyright notice in the Description page of Project Settings.
#include "PlayerCharacterControl.h"
#include "Components/SkeletalMeshComponent.h"
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
#include "InputMappingContext.h"
#include "Components/StaticMeshComponent.h" // 새로 추가
#include "PlayerAnimInstance.h" // 새로 추가
#include "Kismet/GameplayStatics.h" // 새로 추가 사운드 추가할 때 소리 재생 시 필수
#include "Engine/DamageEvents.h" // 아이템 습득 시 추가
// Sets default values
APlayerCharacterControl::APlayerCharacterControl()
{
// Set this character to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;
ConstructorHelpers::FObjectFinder<USkeletalMesh>PlayerMesh(TEXT("/Script/Engine.SkeletalMesh'/Game/Asset/Player/Survival_Character/Meshes/SK_Survival_Character.SK_Survival_Character'"));
if (PlayerMesh.Succeeded())
{
GetMesh()->SetSkeletalMesh(PlayerMesh.Object); // 스켈레톤 보유
GetMesh()->SetWorldLocationAndRotation(FVector(0, 0, -90), FRotator(0, -90, 0)); // 스켈레톤에 이동 및 회전 부여
}
SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm")); // Spring Arm 생성 및 회전 및 각도를 조절하여 Root에 장착
SpringArm->SetupAttachment(RootComponent);
SpringArm->SetWorldLocation(FVector(45, 22, 55.0f));
SpringArm->TargetArmLength = 200;
SpringArm->SocketOffset = FVector(23, 30, 0);
SpringArm->bUsePawnControlRotation = true;
Camera = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera")); // Camera 생성 및 Spring Arm에 장착
Camera->SetupAttachment(SpringArm);
static ConstructorHelpers::FClassFinder<UAnimInstance>AnimInstance(TEXT("/Game/Blueprint/ABP_Player.ABP_Player_C")); // Animation BP 지정; 클래스의 경우는 _C 추가, 경로 상에 '가 들어가있으면 안됨
if (AnimInstance.Class)
{
GetMesh()->SetAnimInstanceClass(AnimInstance.Class);
}
SM_Weapon = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("WeaponMesh")); // 무기 소켓 추가
SM_Weapon->SetupAttachment(GetMesh());
static ConstructorHelpers::FObjectFinder<UStaticMesh>weaponMesh(TEXT("/Script/Engine.StaticMesh'/Game/Asset/FPS_Weapon_Bundle/Weapons/Meshes/AR4/SM_AR4.SM_AR4'")); // 무기 SM 가져옴
if (weaponMesh.Succeeded()) // SM 체크
{
SM_Weapon->SetStaticMesh(weaponMesh.Object);
}
SM_Weapon->AttachToComponent(GetMesh(), FAttachmentTransformRules::KeepRelativeTransform, TEXT("hand_rSocket")); // 소켓에 SM 장착
static ConstructorHelpers::FObjectFinder<UInputMappingContext>InputContext(TEXT("/Script/EnhancedInput.InputMappingContext'/Game/Input/IMC_PlayerInput.IMC_PlayerInput'")); // IMC 지정
if (InputContext.Object != nullptr)
{
DefaultContext = InputContext.Object;
}
static ConstructorHelpers::FObjectFinder<UInputAction>InputMove(TEXT("/Script/EnhancedInput.InputAction'/Game/Input/IA_Move.IA_Move'")); // IA_Move 지정
if (InputMove.Object != nullptr)
{
MoveAction = InputMove.Object;
}
static ConstructorHelpers::FObjectFinder<UInputAction>InputLook(TEXT("/Script/EnhancedInput.InputAction'/Game/Input/IA_Look.IA_Look'")); // IA_Look 지정
if (InputLook.Object != nullptr)
{
LookAction = InputLook.Object;
}
static ConstructorHelpers::FObjectFinder<UInputAction>InputAim(TEXT("/Script/EnhancedInput.InputAction'/Game/Input/IA_Aim.IA_Aim'")); // IA_Aim 지정; OnAim, OffAim 따로 지정해주지 않아도 됨
if (InputAim.Object != nullptr)
{
AimAction = InputAim.Object;
}
static ConstructorHelpers::FObjectFinder<UInputAction>InputFire(TEXT("/Script/EnhancedInput.InputAction'/Game/Input/IA_Fire.IA_Fire'")); // IA_Fire 지정
if (InputFire.Object != nullptr)
{
FireAction = InputFire.Object;
}
static ConstructorHelpers::FObjectFinder<UInputAction>InputOperate(TEXT("/Script/EnhancedInput.InputAction'/Game/Input/IA_Operate.IA_Operate'")); // IA_Operate 지정
if (InputOperate.Object != nullptr)
{
OperateAction = InputOperate.Object;
}
static ConstructorHelpers::FObjectFinder<UInputAction>InputRifleChange(TEXT("/Script/EnhancedInput.InputAction'/Game/Input/IA_RifleChange.IA_RifleChange'")); // IA_지정
if (InputRifleChange.Object != nullptr)
{
WeaponChangeAction = InputRifleChange.Object;
}
IsWeaponMap.Add(EWeaponType::Pistol, false); // 아이템 확인
IsWeaponMap.Add(EWeaponType::SMG, false);
IsWeaponMap.Add(EWeaponType::Rifle, false);
}
// Called when the game starts or when spawned
void APlayerCharacterControl::BeginPlay()
{
Super::BeginPlay();
if (APlayerController* PlayerController = Cast<APlayerController>(GetController())) // PlayerController 관련
{
if(UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
{
Subsystem->AddMappingContext(DefaultContext, 0);
}
}
}
// Called every frame
void APlayerCharacterControl::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
// Called to bind functionality to input
void APlayerCharacterControl::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) // IMC 관련
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent);
EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &APlayerCharacterControl::Move);
EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &APlayerCharacterControl::Look);
EnhancedInputComponent->BindAction(AimAction, ETriggerEvent::Started, this, &APlayerCharacterControl::OnAim); // 조준 관련
EnhancedInputComponent->BindAction(AimAction, ETriggerEvent::Completed, this, &APlayerCharacterControl::OffAim);
EnhancedInputComponent->BindAction(FireAction, ETriggerEvent::Started, this, &APlayerCharacterControl::Fire); // 사격 관련
EnhancedInputComponent->BindAction(OperateAction, ETriggerEvent::Started, this, &APlayerCharacterControl::Operate); // 아이템 감지 관련
EnhancedInputComponent->BindAction(WeaponChangeAction, ETriggerEvent::Started, this, &APlayerCharacterControl::RifleMode);
}
void APlayerCharacterControl::Move(const FInputActionValue& value) // IA_Move 관련 값 계산
{
const FVector2D MovementVector = value.Get<FVector2D>();
const FRotator Rotation = Controller->GetControlRotation();
const FRotator YawRotation(0, Rotation.Yaw, 0);
const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
AddMovementInput(ForwardDirection, MovementVector.Y);
AddMovementInput(RightDirection, MovementVector.X);
}
void APlayerCharacterControl::Look(const FInputActionValue& value) // IA_Look 관련 값 계산
{
FVector2D LookAxisVector = value.Get<FVector2D>();
AddControllerYawInput(LookAxisVector.X * GetWorld()->DeltaTimeSeconds * mousespeed);
AddControllerPitchInput(LookAxisVector.Y * GetWorld()->DeltaTimeSeconds * -mousespeed);
}
void APlayerCharacterControl::OnAim() // 조준 상태 애니메이션 연결
{
OnAim_BP(); // 추가
UPlayerAnimInstance* animInstance = Cast<UPlayerAnimInstance>(GetMesh()->GetAnimInstance()); // 플레이어에게 값 조정으로 변화하는거 체크용
animInstance->isAim = true;
}
void APlayerCharacterControl::OffAim() // 비조준 상태 애니메이션 연결
{
OffAim_BP(); // 추가
UPlayerAnimInstance* animInstance = Cast<UPlayerAnimInstance>(GetMesh()->GetAnimInstance());
animInstance->isAim = false;
}
void APlayerCharacterControl::Fire() // 사격 상태 애니메이션 연결
{
OnFire_BP(); // BP 동시실행 중요중요중요
UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
if (RifleFireMontage)
{
AnimInstance->Montage_Play(RifleFireMontage, 0.233333f);
}
FTimerHandle FireSoundDelayHandle; // 추가
GetWorld()->GetTimerManager().SetTimer(FireSoundDelayHandle, this,
&APlayerCharacterControl::WeaponFireSound,
0.2f,
false
);
}
void APlayerCharacterControl::Operate()
{
if (!isItem)
{
return;
}
Operate_BP();
FVector StartLocation = GetMesh()->GetSocketLocation("hand_r");
FVector EndLocation = StartLocation;
float SphereRadius = 70.0f;
FHitResult HitResult;
FCollisionQueryParams Params;
Params.AddIgnoredActor(this); // 자기자신을 무시
bool isHit = GetWorld()->SweepSingleByChannel(HitResult, StartLocation, EndLocation, FQuat::Identity, ECC_GameTraceChannel9, FCollisionShape::MakeSphere(SphereRadius), Params);
/*DrawDebugSphere(GetWorld(), StartLocation, SphereRadius, 12, FColor::Red, false, 1.0f);*/
if (isHit) // 습득한 아이템과 map의 아이템 일치 여부 확인
{
FString name = HitResult.GetActor()->GetName();
name = name.LeftChop(1);
name = name.LeftChop(name.Len()-name.Find(TEXT("_C_")));
/*if (name.Find(TEXT("BP_")) == 0)
{
name = name.RightChop(3);
}*/
/*name = name.RightChop(name.Len()-name.Find(TEXT("BP_")));
name = name.RightChop(3);*/
/*
* 만약, name에 BP_가 존재한다면, -> if
* 앞에서부터 3개를 잘라라 -> RightChop
*
*/
GEngine->AddOnScreenDebugMessage(-1, 10.0f, FColor::Red, FString::Printf(TEXT("%s"), *name));
if (name==TEXT("BP_Pistol")) // 이름 같은지 체크
{
IsWeaponMap[EWeaponType::Pistol] = true;
}
else if(name == TEXT("SMG"))
{
IsWeaponMap[EWeaponType::SMG] = true;
}
else if (name == TEXT("Rifle"))
{
IsWeaponMap[EWeaponType::Rifle] = true;
}
FTimerHandle HitDelayHandle;
GetWorld()->GetTimerManager().SetTimer(HitDelayHandle,
FTimerDelegate::CreateLambda([HitResult]()
{
HitResult.GetActor()->Destroy();
}),
0.5f,
false
);
}
/*UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
if (OperateMontage)
{
AnimInstance->Montage_Play(OperateMontage, 1.0f);
}*/
}
void APlayerCharacterControl::RifleMode()
{
if (IsWeaponMap.Contains(EWeaponType::Rifle) && IsWeaponMap.Num() > 0 && IsWeaponMap[EWeaponType::Rifle] == true)
{
if (WeaponChangeMontage)
{
UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
AnimInstance->Montage_Play(WeaponChangeMontage, 1.733333f);
}
FTimerHandle HitDelayHandle;
GetWorld()->GetTimerManager().SetTimer(HitDelayHandle,
FTimerDelegate::CreateLambda([this]()
{
if (WeaponChangeSound)
{
UGameplayStatics::PlaySound2D(this, WeaponChangeSound);
}
}),
0.8f,
false
);
}
else // 총이 없는 경우
{
UGameplayStatics::PlaySound2D(this, WeaponChangeSound);
}
}
void APlayerCharacterControl::WeaponFireSound()
{
if (RifleSound)
{
UGameplayStatics::PlaySound2D(this, RifleSound);
//FTimerHandle HitSoundDelayHandle; // 타이머를 제어하기 위한 핸들 변수 (타이머 취소, 타이머 상태 확인) 선언
//GetWorld()->GetTimerManager().SetTimer(HitSoundDelayHandle, // Get world() 현재 객체가 속한 월드를 가져옴 / GetTimerManager() 타이머를 관리하는 시스템 / SetTimer() 타이머를 설정하고 일정 시간이 지나면 특정 작업을 실행
// FTimerDelegate::CreateLambda([this]() { // FTimerDelegate::CreateLambda() 람다변수를 타이머에 연결 가능한 델리게이트로 변환 / [this] 현재 클래스의 멤버 변수 함수에 접근하기 위한 캡쳐 / [=] 모든 외부에 있는 변수 값을 사용함 / () 안에는 내가 원하는 매개변수 삽입가능 / 오류 발생으로 코드 수정
// UGameplayStatics::PlaySound2D(this, HitSound);
// }),
// 1.0f, // 1초 뒤
// false // 반복 x
// ); //소리가 2번 재생되므로 주석처리
// 람다함수 간단한 함수를 선언 없이 즉석해서 만드는 방식 / [캡쳐](매개변수){실행코드};
}
}
|
cs |
PlayerAnimInstance.h (오늘 수정 및 추가한 내용은 없음)
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
|
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Animation/AnimInstance.h"
#include "PlayerAnimInstance.generated.h"
/**
*
*/
UCLASS()
class MYPROJECT_API UPlayerAnimInstance : public UAnimInstance
{
GENERATED_BODY() // 기존
public:
UPlayerAnimInstance();
protected:
virtual void NativeInitializeAnimation() override; // 애니메이션이 생성되면 호출되는 함수
virtual void NativeUpdateAnimation(float DeltaSeconds) override; // 프레임마다 호출 함수
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Character")
class ACharacter* Owner; // Character Actor
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Character")
class UCharacterMovementComponent* Movement; //Movement Component
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Character")
FVector Velocity; // Movement에 Velocity 속력을 저장하는 변수
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Character")
float MoveSpeed; // 속력에 벡터의 2D 구성요소의 길이를 저장하는 변수
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Character")
bool isFalling = false; // Ground에 닿여있는지에 대한 여부
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Character")
float Angle; // Angle 값
public:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Character") // public으로 변경 필수; PlayerControl에서 접근 가능하게 함
bool isAim = false;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Character")
bool isFire = false;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Character") // 상체 회전값을 주기 위해 변수 추가
FRotator AimRotator;
};
|
cs |
PlayerAnimInstance.cpp (오늘 수정 및 추가한 내용은 없음)
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
|
// Fill out your copyright notice in the Description page of Project Settings.
#include "PlayerAnimInstance.h"
#include "GameFramework/Character.h"
#include "GameFramework//CharacterMovementComponent.h"
#include "Kismet/KismetMathLibrary.h" // 새로 추가
UPlayerAnimInstance::UPlayerAnimInstance()
{
}
void UPlayerAnimInstance::NativeInitializeAnimation()
{
Super::NativeInitializeAnimation();
Owner = Cast<ACharacter>(GetOwningActor());
if (Owner)
{
Movement = Owner->GetCharacterMovement();
}
}
void UPlayerAnimInstance::NativeUpdateAnimation(float DeltaSeconds)
{
Super::NativeUpdateAnimation(DeltaSeconds);
if (Movement)
{
Velocity = Movement->Velocity;
MoveSpeed = Velocity.Size2D();
isFalling = Movement->IsFalling();
//이동 방향에 따른 캐릭터 방향 계산
Angle = UAnimInstance::CalculateDirection(Velocity, Owner->GetActorRotation()); // Velocity: 현재 캐릭터의 속도 벡터, Owner->GetActorRotation(): 현재 액터의 회전값(방향), Velocity가 현재 회전 방형과 얼마나 차이 나는지도 반환
FRotator DeltaRocator = UKismetMathLibrary::NormalizedDeltaRotator(Owner->GetActorRotation(), Owner->GetControlRotation()); // 카메라 방향과 캐릭터 방향의 차이를 정규화 계산
(Owner->GetActorRotation(), // 기준이 되는 방향, 캐릭터의 현재 회전
Owner->GetControlRotation()); // 목표 방향, 플레이어가 컨트롤하는 방향 (카메라 방향)
AimRotator = FRotator(0, DeltaRocator.Yaw, DeltaRocator.Pitch);
}
}
|
cs |
SoundAnimNotifyState.h (오늘 수정 및 추가한 내용은 없음)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Animation/AnimNotifies/AnimNotifyState.h"
#include "SoundAnimNotifyState.generated.h"
/**
*
*/
UCLASS()
class MYPROJECT_API USoundAnimNotifyState : public UAnimNotifyState
{
GENERATED_BODY()
public:
virtual void NotifyBegin(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, float TotalDuration) override; // NotifyState가 시작될 때 호출 <- 총소리를 begin에 추가
virtual void NotifyTick(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, float FrameDeltaTime) override; // Begin() 함수가 호출된 후 End가 울리기 전까지 반복 실행
virtual void NotifyEnd(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation) override; // NotifyState 구간이 끝날 때 호출
};
|
cs |
SoundAnimNotifyState.cpp (오늘 수정 및 추가한 내용은 없음)
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
|
// Fill out your copyright notice in the Description page of Project Settings.
#include "SoundAnimNotifyState.h"
#include "Engine/GameEngine.h"
#include "Kismet/GameplayStatics.h"
#include "PlayerCharacterControl.h"
void USoundAnimNotifyState::NotifyBegin(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, float TotalDuration)
{
//GEngine->AddOnScreenDebugMessage(-1, 1.0f, FColor::Red, TEXT("NotifyBegin"));
//APlayerCharacterControl* player = Cast<APlayerCharacterControl>(UGameplayStatics::GetPlayerCharacter(GetWorld(), 0));
//if (player)
//{
//player->WeaponFireSound();
//} //새로추가한 것
}
void USoundAnimNotifyState::NotifyTick(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, float FrameDeltaTime)
{
//GEngine->AddOnScreenDebugMessage(-1, 1.0f, FColor::Red, TEXT("NotifyTick"));
}
void USoundAnimNotifyState::NotifyEnd(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation)
{
//GEngine->AddOnScreenDebugMessage(-1, 1.0f, FColor::Red, TEXT("NotifyEnd"));
}
|
cs |