TIL(40)

이채원·2026년 8월 13일

TIL

목록 보기
40/77

오늘의 할일
1. 코드카타 정리
2. 세션 정리


코드카타 정리

매개변수 left부터 right까지의 모든 수들 중에서, 약수의 개수가 짝수인 수는 더하고, 약수의 개수가 홀수인 수는 뺀 수를 return 하도록 하는 함수

-> 제일 먼저 떠오른 게 count를 하나씩 늘려가서 그 count로 짝수 홀수를 가려내자 라고 생각함

#include <string>
#include <vector>

using namespace std;

int solution(int left, int right) {
    int answer = 0;
    for (int i = left; i <= right; i++) {
        int count = 0;
        
        for (int j = 1; j <= i; j++) {
            if (i % j == 0) {
                count++;
            }
        }
        
        if (count % 2 == 0) {
            answer += i;
        } else {
            answer -= i;
        }
    }
    return answer;
}

AI가 만든 코드

#include <cmath>

using namespace std;

int solution(int left, int right) {
    int answer = 0;
    
    // 후위 연산자(i++) 대신 전위 연산자(++i)를 사용하여 미세한 성능 최적화
    for (int i = left; i <= right; ++i) {
        int root = sqrt(i);
        
        // 삼항 연산자를 통해 완전제곱수면 빼고(-i), 아니면 더합니다(+i)
        answer += (root * root == i) ? -i : i; 
    }
    
    return answer;
}

💡 이 방식을 선택한 3가지 이유
1. 압도적인 성능 (O(N)): 약수를 일일이 세지 않게 하기
2. 삼항 연산자 사용: 코드가 위아래로 길어지는 if-else 블록 대신 (조건) ? 참일 때 값 : 거짓일 때 값 형태의 삼항 연산자를 사용하여, 변수 answer에 값이 어떻게 누적되는지 한 줄로 보여줄 수 있음
3. ++i를 쓰는 습관: for문에서 흔히 쓰는 i++ 대신 ++i를 사용. 기본 정수형(int)에서는 속도 차이가 거의 없지만, 나중에 무거운 객체(이터레이터 등)를 반복문으로 돌릴 때 ++i를 쓰는 것이 복사본을 만들지 않아 메모리 측면에서 훨씬 유리.


C++와 Unreal Engine으로 3D 게임 개발 : 3회차 정리

2-3 캐릭터 동작 구현과 입력 처리

  1. GameMode - Character, Player Controller
  2. Character - GameMode - 월드에 스폰
  3. Player Controller - 사용자의 입력과 Character 사이에서 연결
  4. IMC, IA 입력 매핑

IMC 활성화
Local Player - Local Player Subsystem - IMC 활성화
Loacal Player = 사용자

  1. 사용자가 A 버튼을 누름
  2. Player Controller A 인식
  3. Local Player Subsystem <- A가 무슨 뜻이야?
  4. ㅇㅇ 동작임.
  5. Player Controller -> Character A는 이런 뜻이니까 거기에 맞는 행동 함수를 호출해 //이게 아직 구현 안돼서 오늘은 이걸 구현할거임
  6. Character A에 맞는 함수를 실행.
  • FInputActionValue : Enhanced Input에서 액션 값 (축 이동값, 마우스 이동량 등)을 전달할 때 사용하는 구조체 (IA에서 설정한 Value Type)
class USpringArmComponent;
class UCameraComponent;
struct FInputActionValue;	// Enhanced Input에서 액션 값을 받을 때 사용하는 구조체

protected:
	// 입력 바인딩을 처리할 함수
	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

	// IA_Move와 IA_Jump 등을 처리할 함수 원형
    // Enhanced Input에서 액션 값은 FInputActionValue로 전달

	UFUNCTION()	//입력 바인딩 함수->안붙이면 바인딩 실패됨
	void Move(const FInputActionValue& value);
	UFUNCTION()
	void StartJump(const FInputActionValue& value);
	UFUNCTION()
	void StopJump(const FInputActionValue& value);
	UFUNCTION()
	void Look(const FInputActionValue& value);
	UFUNCTION()
	void StartSprint(const FInputActionValue& value);
	UFUNCTION()
	void StopSprint(const FInputActionValue& value);

};
  • Jump, Sprint는 누르고 있는 동안/ 손 뗀 순간 이렇게 두 가지로 나뉨
  • BindAction 함수
    -- 첫 번째 인자: 어떤 UInputAction과 연결할지
    -- 두 번째 인자: 액션이 발생하는 트리거 이벤트 (Triggered, Ongoing, Completed 등).
    -- 세 번째/네 번째 인자: 액션 발생 시 실행할 객체(this)와 함수 포인터
#include "SpartaCharacter.h"
#include "SpartaPlayerController.h"
#include "EnhancedInputComponent.h"
#include "Camera/CameraComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "GameFramework/SpringArmComponent.h"

void ASpartaCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);
	
    // Enhanced InputComponent로 캐스팅
	if (UEnhancedInputComponent* EnhancedInput = Cast<UEnhancedInputComponent>(PlayerInputComponent)) {
    	// IA를 가져오기 위해 현재 소유 중인 Controller를 ASpartaPlayerController로 캐스팅
		if (ASpartaPlayerController* PlayerController = Cast<ASpartaPlayerController>(GetController())) {
			if (PlayerController->MoveAction) {
            	// IA_Move 액션 키를 "키를 누르고 있는 동안" Move() 호출
				EnhancedInput->BindAction(	//이벤트랑 함수를 연결하는 핵심 코드
					PlayerController->MoveAction,	//IA 가져온 것
					ETriggerEvent::Triggered,	//키가 눌려서 이벤트가 발생했을 때
					this,	//호출된 함수의 객체
					&ASpartaCharacter::Move	//Move를 연결시키겠다
				);
			}

			if (PlayerController->JumpAction) {
				EnhancedInput->BindAction(	
					PlayerController->JumpAction,	
					ETriggerEvent::Triggered,	
					this,	
					&ASpartaCharacter::StartJump
				);

				EnhancedInput->BindAction(
					PlayerController->JumpAction,
					ETriggerEvent::Triggered,
					this,
					&ASpartaCharacter::StopJump
				);

			}

			if (PlayerController->LookAction) {
				EnhancedInput->BindAction(
					PlayerController->LookAction,
					ETriggerEvent::Triggered,
					this,
					&ASpartaCharacter::Look
				);
			}

			if (PlayerController->SprintAction) {
				EnhancedInput->BindAction(
					PlayerController->SprintAction,
					ETriggerEvent::Triggered,
					this,
					&ASpartaCharacter::StartSprint
				);

				EnhancedInput->BindAction(
					PlayerController->SprintAction,
					ETriggerEvent::Triggered,
					this,
					&ASpartaCharacter::StopSprint
				);

			}
		}
	}
}

캐릭터의 이동 함수 구현

어제 만들었던 컨트롤러와 IA, IMC를 바탕으로 코드 짜기(2D, bool 등)

  • .h 파일에 변수 정의하기
//Sprint 함수 만들 때 필요한 변수 정의
private:
	float NormalSpeed;				// 기본 걷기 속도
	float SprintSpeedMultiplier;	// "기본 속도" 대비 몇 배로 빠르게 달릴지 결정
	float SprintSpeed;				// 실제 스프린트 속도
  • .cpp 생성자 함수에 헤더파일에서 정의했던 변수 할당하기
#include "SpartaCharacter.h"
#include "SpartaPlayerController.h"
#include "EnhancedInputComponent.h"
#include "Camera/CameraComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "GameFramework/SpringArmComponent.h"

ASpartaCharacter::ASpartaCharacter()
{
	CameraComp->bUsePawnControlRotation = false;

	//변수 할당하기(생성자)
	NormalSpeed = 600.0f;	 
	SprintSpeedMultiplier = 1.7f;		//기본 속도의 1.7배
	SprintSpeed = NormalSpeed * SprintSpeedMultiplier;
	
	GetCharacterMovement()->MaxWalkSpeed = NormalSpeed;
}
  • FInputActionValue::Get<FVector2D>() : 2차원 벡터 형태 입력받기
  • GetCharacterMovement() : ACharacter 클래스에 기본 내장된 함수
    -> UCharacterMovementComponent* 포인터를 반환. MaxWalkSpeed 등의 이동 관련 설정 체인지 가능
  • AddMovementInput() : 키 입력 때마다 어느 방향으로 이동할지 전달하는 함수
    -> MaxWalkSpeed : 최대 이동 속도가 얼마인지.
void ASpartaCharacter::Move(const FInputActionValue& value) {
	if (!Controller) return;

	const FVector2D MoveInput = value.Get<FVector2D>();

	if (!FMath::IsNearlyZero(MoveInput.X)) {
    	//AddMovementInput(방향, 크기)
        //방향 : 월드 좌표 기준 이동 방향(Forward, Right 등)
        //크기 : 이동 스케일(속도)
        //내부적으로 CharacterMovementComponent가 이 명령어로 속도 계산하고, 실제 이동을 구현
		AddMovementInput(GetActorForwardVector(), MoveInput.X);
	}

	if (!FMath::IsNearlyZero(MoveInput.Y)) {
		AddMovementInput(GetActorRightVector(), MoveInput.Y);
	}
}

void ASpartaCharacter::StartJump(const FInputActionValue& value) {
	if (value.Get<bool>()) {
		Jump();
	}
}

void ASpartaCharacter::StopJump(const FInputActionValue& value) {
	if (!value.Get<bool>()) {
		StopJumping();
	}
}
//왜 Jump에서는 Controller 체크를 안했냐고 하면 일단 이 점프랑 스탑 점핑이 내부적으로 구현된, 이 엔진에서 구현된 이 코드 내부에
// //Controller 체크가 한 번 있음
//그래서 그런 탓도 있지만 GetActorForwardVector 같은 걸 쓰려면 밖에서 Controller 체크를 또 해줘야함
//점프는 그런 걸 안 쓰기 때문에 구현 안한 거임

void ASpartaCharacter::Look(const FInputActionValue& value) {
	FVector2D LookInput = value.Get<FVector2D>();
    //AddControllerYawInput(): 카메라의 Yaw 축(수평 회전)을 변경
	AddControllerYawInput(LookInput.X);
    //AddControllerPitchInput(): 카메라의 Pitch 축(수직 회전)을 변경
	AddControllerPitchInput(LookInput.Y);
}

void ASpartaCharacter::StartSprint(const FInputActionValue& value) {
	//스프린트 속도
	if (GetCharacterMovement()) {
		GetCharacterMovement()->MaxWalkSpeed = SprintSpeed;
	}
}

void ASpartaCharacter::StopSprint(const FInputActionValue& value) {
	// 평상시 속도
    if (GetCharacterMovement()) {
		GetCharacterMovement()->MaxWalkSpeed = NormalSpeed;
	}
}

응용반 : 1회차 정리

1. 언리얼 엔진 C++과 빌드 시스템(컴파일러)

  • UBT(Unreal Build Tool) : 언리얼 엔진의 전체적인 빌드 과정을 관리하는 툴
  • UHT(Unreal Header Tool) : UCLASS, USTRUCT, UENUM, UPROPERTY, UFUNCTION 같은 매크로가 들어간 헤더 파일을 분석하여 리플렉션용 코드를 자동 생성해줌

UBT가 UHT 실행 → 리플렉션용 코드 생성 → C++ 컴파일러 실행

2. 언리얼 c++ 컴파일의 2단계 과정

언리얼 엔진 내부의 컴파일러(UBT, UHT)가 기본 C++ 코드 외의 언리얼 규격 코드를 해석하고 변환
1) UHT 실행 : UBT가 먼저 UHT를 실행해서 언리얼 관련 헤더를 분석하고 UObject 시스템에 필요한 코드를 생성
-> 모든 코드를 포함할 때 발생하는 성능 이슈를 방지하기 위해

2) C++ 컴파일 실행 : 일반 C++ 컴파일러가 호출되어서 사용자가 작성한 프로젝트 코드와 UHT가 자동 생성한 코드를 함께 컴파일

  • UBT (작업 반장): 전체 프로젝트의 변경 사항을 추적해서 "수정된 부분만" 다시 컴파일하도록 지휘(컴퓨터 안 터지게 방지). 흩어진 부품을 모아주는 역할(빌드 시스템)

  • UHT (번역가/정보원): 매크로(UCLASS 등)가 달린 코드를 읽고 "이건 언리얼에서 쓸 수 있는 클래스" 하고 엔진이 알아먹을 수 있는 부가 코드를 자동으로 짜주는(알려주는) 역할

3. 코드 수정 시 컴파일 : Live Coding vs IDE 빌드

1) 구현만 변경했을 때 -> Live Coding

  • 변경 대상 : 함수 내부 로직 수정, 조건문 변경, 알고리즘 수정, 로그 추가 (.cpp 파일 수정)
  • 특징 : 에디터가 실행 중이어도 C++ 코드를 빌드하고 실행 중인 바이너리에 빠르게 패치. 반복 테스트가 많은 게임플레이 로직 작업 시 효율적임
  • 주의할 점 : 변수의 기본값을 .cpp 생성자에서 바꾼 경우, 이미 씬에 존재하는 오브젝트 인스턴스에는 그 값이 바로 반영되지 않음

2) 구조나 헤더가 변경됐을 때 -> 에디터 종료 후 IDE 빌드(전체 빌드 하기)

  • 변경 대상 : 변수 추가/삭제, 함수 선언 변경, 클래스 상속 구조 변경, 매크로(UPROPERTY 등) 추가/변경, 클래스 설계 자체 변경(.h 파일 수정)
  • 특징 : 단순 구현 변경이 아니라 UHT가 다시 코드를 해석하고 생성해야 하는 영역이므로 영향 범위가 큼. 전체 빌드가 안전함.

4. 클래스 생성 주의사항

새 클래스를 추가할 때는 반드시 언리얼 에디터(VOD)를 통해 생성해야함!!!
에디터를 통해 생성해야 헤더/소스 파일 생성과 함께 게임 모듈 및 프로젝트 파일 갱신이 정상적으로 이루어짐.

  • 수동 추가를 피해야 하는 이유 :
    새 클래스에 필요한 헤더, 소스, 게임 모듈이 자동으로 업데이트되지 않음
    UHT 자동 연동이 누락되면서 프로젝트 파일 갱신이나 에디터-IDE 간 인식 상태가 어긋남
    만약 수동으로 추가했다면 .uproject 파일을 우클릭해서 Generate Project Files를 실행하거나 모듈 설정을 직접 수정해야함

5. 게임 파이프라인 용어 정리

  • 컴파일 : C++ 코드나 블루프린트 그래프를 실행 가능한 형태로 변환하는 과정
  • 쿠킹 : 텍스처, 메시, 사운드, 블루프린트, 레벨 등의 콘텐츠를 타겟 플랫폼(PC, 모바일, 콘솔 등)에서 사용할 수 있는 형식으로 변환
    -> 타겟 기기에 맞게 압축하고 변형하는 필수 과정
  • 패키징 : 컴파일된 코드와 쿠킹된 콘텐츠를 배포 가능한 최종 파일 형태로 묶는 작업 (일반적인 순서 : Build -> Cook -> Stage -> Package -> Deploy -> Run)
  • 로드 : 게임 실행 중 필요한 에셋이나 레벨을 저장 장치에서 메모리로 가져오는 작업

6. 블루프린트와 셰이더의 컴파일

  • 블루프린트 컴파일 : 게임에서 사용되기 전, 노드 연결/변수/함수/이벤트 그래프를 검사하고 실행 가능한 코드로 생성.
    (예시 : 배그는 블루프린트 성능 이슈를 해결하기 위해 C++로 전환 -> 10배 성능 향상)

  • 셰이더 컴파일 : c++ 컴파일이나 에셋 로드와는 별개의 작업. 캐시된 셰이더 맵이 없는 머티리얼이 로드되면 큐에 들어가며, 'Shader Complie Worker' 프로세스를 통해 GPU용 코드를 병렬로 준비

++ 머티리얼도 별도의 컴파일 과정 존재함

7. 리플렉션 & 가비지 컬렉션

리플렉션 : 프로그램이 런타임에 자기 자신(클래스, 변수, 함수 정보 등)에 대한 정보를 알 수 있게 해주는 기능. 언리얼은 UHT와 매크로를 통해서 리플렉션 시스템 구축함.

  • 기능
  • C++ 변수를 에디터 디테일 패널에 노출
  • C++ 함수를 블루프린트에서 호출
  • C++ 클래스를 블루프린트 부모로 사용
  • 네트워크 복제 대상 변수 관리, 변수 저장/불러오기
  • 런타임 클래스 타입 확인 및 에디터 속성 검색

가비지 컬렉션 : 더이상 사용되지 않는 오브젝트를 엔진이 자동으로 감지해서 메모리에서 정리해 주는 메모리 관리 시스템
-> 엔진이 "이게 쓰레기인지 아닌지" 알 수 있게 만들려면 코드에 UPROPERTY() 같은 매크로를 달아줘서 수동 등록을 해줘야 함.

8. 매크로 및 헤더 파일 규칙

  • #pragme once : 전처리 지시문. 헤더 파일이 중복으로 인클루드되는 것을 방지
  • .generated.h : 헤더 파일에서 무조건 가장 마지막에 인클루드되어야 함. 아니면 컴파일 에러 발생
    -> UHT가 생성한 리플렉션 코드 & 언리얼 오브젝트 시스템 지원 코드 포함
  • UCLASS() : C++ 클래스를 언리얼 오브젝트 시스템(UObject)에 등록하는 매크로. 이 매크로를 통해서 엔진, 에디터, 블루프린트, 리플렉션, 가비지 컬렉션 시스템과 연결됨.
UCLASS()
class CROSSFIRE_API ATestActor : public AActor
{
    GENERATED_BODY()
};

//ATestActor는 AActor를 상속
//클래스가 언리얼의 에디터, 블루프린트, 리플렉션, 가비지 컬렉션 시스템과 연결이 되려면 
//엔진이 이 클래스를 알아야 알 수 있음
//그 표시가 UCLASS()
  • 클래스 지정자 : UCLASS() 괄호 안에 들어가며 엔진/에디터에서의 동작 제어.
    (ex : Blueprintable - 상속 가능, NotBlueprintable - 상속 불가능, BlueprintType - 변수 타입으로 사용 가능)
  • _API 매크로 : 모듈 API 지정자. (ex: MYPROJECT_API) 해당 클래스나 함수를 다른 모듈(Core, Engine, UMG 등)의 DLL 파일에 Public으로 노출해서 외부에서 사용할 수 있게끔 함

!!최종 컴파일 파이프라인 요약!!
컴파일 : UBT가 빌드 -> UHT가 코드를 언리얼에 맞게 생성 -> 헤더와 매크로 읽어서 처리 -> 컴파일러가 .cpp 변환 -> 어셈블 -> DLL 생성

profile
초보 개발자

0개의 댓글