TIL(61)

이채원·2026년 9월 16일

TIL

목록 보기
62/77

오늘의 할일
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) 생성

  • PuzzleScareSpawner.h
#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;
};
  • PuzzleScareSpawner.cpp
#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를 생성

  • WeepingAngelZombie.h
#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;
};
  • WeepingAngelZombie.cpp
#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;
}

다음 학습 목표

스크럼 하면서(통합하면서) 수정사항 발생함..!

  1. 우는천사 데미지 즉사 처리
  2. 우는천사 에셋 확정
  3. 우는천사 파일들 폴더 AI로 옮기기

작업하고 정전귀신순간이동, 소리를 이용한 가짜 점프 스케어까지 작업 완료하면

엔딩 영상 작업 시작하기

profile
초보 개발자

0개의 댓글