오늘의 할일
1. 코드카타 정리
2. 팀프로젝트 정리
3. 다음 학습 목표
코니는 영어 단어가 적힌 카드 뭉치 두 개를 선물로 받았습니다. 코니는 다음과 같은 규칙으로 카드에 적힌 단어들을 사용해 원하는 순서의 단어 배열을 만들 수 있는지 알고 싶습니다.
원하는 카드 뭉치에서 카드를 순서대로 한 장씩 사용합니다.
한 번 사용한 카드는 다시 사용할 수 없습니다.
카드를 사용하지 않고 다음 카드로 넘어갈 수 없습니다.
기존에 주어진 카드 뭉치의 단어 순서는 바꿀 수 없습니다.
예를 들어 첫 번째 카드 뭉치에 순서대로 ["i", "drink", "water"], 두 번째 카드 뭉치에 순서대로 ["want", "to"]가 적혀있을 때 ["i", "want", "to", "drink", "water"] 순서의 단어 배열을 만들려고 한다면 첫 번째 카드 뭉치에서 "i"를 사용한 후 두 번째 카드 뭉치에서 "want"와 "to"를 사용하고 첫 번째 카드뭉치에 "drink"와 "water"를 차례대로 사용하면 원하는 순서의 단어 배열을 만들 수 있습니다.
문자열로 이루어진 배열 cards1, cards2와 원하는 단어 배열 goal이 매개변수로 주어질 때, cards1과 cards2에 적힌 단어들로 goal를 만들 있다면 "Yes"를, 만들 수 없다면 "No"를 return하는 solution 함수를 완성해주세요.
#include <string>
#include <vector>
using namespace std;
string solution(vector<string> cards1, vector<string> cards2, vector<string> goal) {
// 왼손(idx1)과 오른손(idx2)이 각각 카드의 맨 위(0번)를 가리킴
int idx1 = 0;
int idx2 = 0;
// 완성해야 할 goal 배열의 단어를 처음부터 하나씩 확인
for (int i = 0; i < goal.size(); i++) {
string target = goal[i]; // 이번 턴에 코니가 외쳐야 할 단어
// 왼손(cards1)의 맨 위 카드와 목표 단어가 같은지 확인
// 카드를 다 써버린 경우 제외
if (idx1 < cards1.size() && cards1[idx1] == target) {
idx1++; // 카드를 썼으므로 왼손을 다음 카드로 한 칸 내림
}
// 왼손에 없으면 오른손(cards2)의 맨 위 카드와 같은지 확인
else if (idx2 < cards2.size() && cards2[idx2] == target) {
idx2++; // 카드를 썼으니 오른손을 다음 카드로 한 칸 다운
}
// 양쪽 뭉치의 맨 위 카드 둘 다 맞지 않다면 실패
else {
return "No";
}
}
// goal에 있는 모든 단어를 찾았다면 성공
return "Yes";
}
퍼즐 완료 직후 공격 전투담당+몬스터(ai)담당+ 퍼즐담당
퍼즐을 푼다 -> 문 열림 -> 성공효과음 -> 플레이어 안심 -> 뒤에서 소리 -> 적 추격 시작
우는 천사(공격불가 도망만 가능) 전투담당+몬스터(ai)담당
플레이어가 적을 바라본다 -> 적이 가만히 있는다 플레이어가 다른곳을 본다 -> 적이 빠르게 다가온다
좁은곳에서 이동시 등장하는 점프스케어 전투담당+몬스터(ai)담당
비좁은곳,작은 구멍을 지나간다 -> 바로 앞에서 괴물이 모습을 드러내고 사라짐
창문 너머 공포의 존재 전투담당+몬스터(ai)담당
진료실 유리창 너머에 누군가 지나가는 형체가 보임
천장 환풍구/ 문 돌파형 전투담당+몬스터(ai)담당+플레이어 시각효과x 청각효과o
문,환풍구 근처에 접근한다 -> 문,환풍구를 부수면서 좀비가 달려든다.
뒤돌아보기형 전투담당+몬스터(ai)담당
벽에 피로 뒤를 봐라는 메세지를 발견한다 -> 플레이어가 뒤를 돌면 귀신이나 깝툭튀 요소가 등장한다 or 불이꺼짐 or 비명소리
시야 밖 환경 변화형 전투담당 + 맵/레벨 담당 추가사항(시간 남으면)
플레이어가 방을 본다 -> 다른곳을 보다가 다시 본다 -> 방 구조,물건 위치,갯수 등이 바뀐다
정전 -> 등장형 전투담당+몬스터(ai)담당
복도-> 불깜빡 -> 멀리서 귀신 등장 -> 불꺼짐 -> 불켜짐 -> 중간거리 귀신 -> 불꺼짐 -> 불켜짐 -> 바로 앞 귀신 -> 소멸
소리를 이용한 가짜 점프 스케어 전투담당+맵/레벨 담당
뒤에서 쾅하고 문닫히는 소리 -> 플레이어가 급하게 뒤돌아봄 -> 아무것도 없음 아무것도 없는 폐병원에서 소녀의 콧노래 소리가 들림 -> 노래가 들리는 곳으로 가봤지만 아무것도 없음
정면 돌진형 전투담당+몬스터(ai)담당 문을 열거나 코너를 돌았을 때 적이 바로 앞으로 달려든다
퍼즐 점프 스케어 스포너(APuzzleScareSpawner) 생성
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "PuzzleScareSpawner.generated.h"
class AZombieCharacter;
class APlayerCharacter;
class USoundBase;
UCLASS()
class DEAD_HOSPITAL_API APuzzleScareSpawner : public AActor
{
GENERATED_BODY()
public:
APuzzleScareSpawner();
// 소환할 좀비 클래스
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "JumpScare|Puzzle")
TSubclassOf<AZombieCharacter> ZombieClassToSpawn;
// 등 뒤에서 들릴 사운드 (발소리, 괴성 등)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "JumpScare|Puzzle")
USoundBase* ScareSound;
// 퍼즐 완료 후 적이 등장하기까지의 안심 시간 (기본값 2.5초)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "JumpScare|Puzzle")
float DelayBeforeScare = 2.5f;
// 등 뒤 얼만큼 떨어져서 스폰될지
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "JumpScare|Puzzle")
float SpawnDistance = 400.0f;
// 퍼즐 성공 시 호출해 줄 함수
UFUNCTION(BlueprintCallable, Category = "JumpScare|Puzzle")
void OnPuzzleSolved(APlayerCharacter* Player);
private:
// 안심 시간이 끝난 뒤 실제로 적을 소환하는 내부 함수
void ExecuteScare();
// 타이머가 돌 동안 플레이어 정보를 들고 있을 변수
APlayerCharacter* TargetPlayer;
// 시간차 공격을 위한 타이머 핸들
FTimerHandle ScareTimerHandle;
};
#include "PuzzleScareSpawner.h"
#include "TimerManager.h"
#include "Kismet/GameplayStatics.h"
#include "../AI/ZombieCharacter.h"
#include "PlayerCharacter.h"
APuzzleScareSpawner::APuzzleScareSpawner()
{
PrimaryActorTick.bCanEverTick = false;
}
void APuzzleScareSpawner::OnPuzzleSolved(APlayerCharacter* Player)
{
if (Player == nullptr || ZombieClassToSpawn == nullptr) return;
TargetPlayer = Player; // 타이머가 끝난 뒤에 쓰기 위해 플레이어 저장
// 설정된 시간(DelayBeforeScare)만큼 기다렸다가 ExecuteScare 실행
GetWorld()->GetTimerManager().SetTimer(
ScareTimerHandle,
this,
&APuzzleScareSpawner::ExecuteScare,
DelayBeforeScare,
false
);
UE_LOG(LogTemp, Warning, TEXT("퍼즐 완료 신호 수신. %f초 뒤 점프 스케어가 발동됩니다..."), DelayBeforeScare);
}
void APuzzleScareSpawner::ExecuteScare()
{
if (TargetPlayer == nullptr) return;
// 플레이어 등 뒤 좌표 계산
FVector PlayerLocation = TargetPlayer->GetActorLocation();
FVector PlayerForward = TargetPlayer->GetActorForwardVector();
FVector SpawnLocation = PlayerLocation - (PlayerForward * SpawnDistance);
SpawnLocation.Z = PlayerLocation.Z;
FRotator SpawnRotation = (PlayerLocation - SpawnLocation).Rotation();
// 등 뒤(스폰될 위치)에서 사운드 재생
if (ScareSound)
{
UGameplayStatics::PlaySoundAtLocation(this, ScareSound, SpawnLocation);
}
// 좀비 소환
FActorSpawnParameters SpawnParams;
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;
AZombieCharacter* SpawnedZombie = GetWorld()->SpawnActor<AZombieCharacter>(
ZombieClassToSpawn,
SpawnLocation,
SpawnRotation,
SpawnParams
);
if (SpawnedZombie)
{
UE_LOG(LogTemp, Warning, TEXT("플레이어 등 뒤에 좀비가 나타났습니다."));
}
}
AI/몬스터 담당자 :
좀비가 허공에서 갑자기 스폰되는 방식이라서 소환되자마자 즉시 플레이어를 인지하고 달려들 수 있도록 좀비 블루프린트 Event BeginPlay에서 어그로가 바로 끌리게 세팅
퍼즐/레벨 담당자 :
퍼즐 기믹 옆에 PuzzleScareSpawner 액터를 배치하고 디테일 패널에서 소환할 좀비 클래스랑 딜레이 시간(몇 초 뒤에 놀래킬지), 그리고 등 뒤에서 들릴 사운드를 세팅해 주시면 됩니다. 퍼즐을 풀고 문이 열리는 순간, 블루프린트에서 PuzzleScareSpawner의 OnPuzzleSolved(Player) 함수를 한 번 호출하면 됩니다
전달 완료!
부모 클래스를 ZombieCharacter로 선택하여 WeepingAngelZombie를 생성
#pragma once
#include "CoreMinimal.h"
#include "../AI/ZombieCharacter.h"
#include "WeepingAngelZombie.generated.h"
UCLASS()
class DEAD_HOSPITAL_API AWeepingAngelZombie : public AZombieCharacter
{
GENERATED_BODY()
public:
AWeepingAngelZombie();
// 매 프레임 시야를 검사하기 위해 Tick 함수 오버라이드
virtual void Tick(float DeltaTime) override;
// 공격 불가 처리 -> 데미지 파이프라인 오버라이드
virtual float TakeDamage(float DamageAmount, struct FDamageEvent const& DamageEvent, class AController* EventInstigator, AActor* DamageCauser) override;
// 시야에서 벗어났을 때의 추격 속도
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "JumpScare|Angel")
float ChaseSpeed = 800.0f;
private:
// 플레이어가 나를 보고 있는지 계산하는 함수
bool CheckIfSeenByPlayer();
// 상태 변화를 감지하기 위한 이전 프레임 상태 저장용 변수
bool bWasSeenLastFrame = false;
};
#include "WeepingAngelZombie.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "Kismet/GameplayStatics.h"
#include "PlayerCharacter.h"
AWeepingAngelZombie::AWeepingAngelZombie()
{
PrimaryActorTick.bCanEverTick = true;
}
// 공격 불가
float AWeepingAngelZombie::TakeDamage(float DamageAmount, struct FDamageEvent const& DamageEvent, class AController* EventInstigator, AActor* DamageCauser)
{
// 아무리 데미지가 들어와도 0.0f를 반환하여 체력이 깎이지 않게 방어
UE_LOG(LogTemp, Warning, TEXT("우는 천사는 공격할 수 없습니다"));
return 0.0f;
}
// 얼음/땡 처리
void AWeepingAngelZombie::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
bool bIsSeenNow = CheckIfSeenByPlayer();
// 상태가 변했을 때만 속도와 애니메이션 업데이트
if (bIsSeenNow != bWasSeenLastFrame)
{
if (bIsSeenNow)
{
// [얼음] 플레이어가 쳐다볼 때: 발을 묶고 애니메이션을 멈춤
GetCharacterMovement()->MaxWalkSpeed = 0.0f;
GetMesh()->bPauseAnims = true;
}
else
{
// [땡] 시야에서 벗어났을 때: 돌진하며 애니메이션 재생
GetCharacterMovement()->MaxWalkSpeed = ChaseSpeed;
GetMesh()->bPauseAnims = false;
}
bWasSeenLastFrame = bIsSeenNow;
}
}
// 시야 판정
bool AWeepingAngelZombie::CheckIfSeenByPlayer()
{
APlayerCharacter* Player = Cast<APlayerCharacter>(UGameplayStatics::GetPlayerCharacter(GetWorld(), 0));
if (Player == nullptr) return false;
FVector PlayerLocation;
FRotator PlayerRotation;
Player->GetActorEyesViewPoint(PlayerLocation, PlayerRotation);
FVector DirToAngel = (GetActorLocation() - PlayerLocation).GetSafeNormal();
FVector PlayerForward = PlayerRotation.Vector();
// 시야각(FOV) 검사
// 내적 값이 0.5 이상이면 정면을 기준으로 약 60도 원뿔 시야 내에 있다는 뜻
float DotProduct = FVector::DotProduct(PlayerForward, DirToAngel);
if (DotProduct > 0.5f)
{
// 장애물(벽) 가림 검사
FHitResult HitResult;
FCollisionQueryParams QueryParams;
QueryParams.AddIgnoredActor(Player); // 플레이어 자신은 통과
bool bHit = GetWorld()->LineTraceSingleByChannel(
HitResult,
PlayerLocation,
GetActorLocation(),
ECC_Visibility,
QueryParams
);
// 시야에 있고 + 광선을 쐈을 때 벽에 막히지 않고 우는 천사를 정확히 맞췄다면
if (bHit && HitResult.GetActor() == this)
{
return true; // 플레이어가 나를 쳐다보고 있다는 뜻
}
}
return false;
}
스크럼 하면서(통합하면서) 수정사항 발생함..!
작업하고 정전귀신순간이동, 소리를 이용한 가짜 점프 스케어까지 작업 완료하면
엔딩 영상 작업 시작하기