이제 본격적으로 캐릭터를 움직이게 해보자

 

일단 새를 시점으로 플레이할 수 있도록 새의 auto possess player를 Player0으로 설정해주자

이것을 코드로 구현하게되면 아래와 같다.

	AutoPossessPlayer = EAutoReceiveInput::Player0;

1. 키바인딩

 

만약 컨트롤러가 2개이상이라면 첫번째 컨트롤러가 첫번째 플레이어 두번째 컨트롤러가 두번째 컨트롤러를 사용하게 된다.

그리고 이제 키를 Mapping해주자 키는 축 매핑에서 추가해주자

키 매핑
키 매핑과 함수연관성

키를 통해 움직이게 하는 콜백하려면 콜백 함수를 할당해주어야 한다.

콜백 함수를 붙여줄 때는 SetupPlayerInputComponent을 통해 한다.

이때 우리가 매핑해준 키의 이름을 잘 알고있어야한다. 지금은 MoveForWard를 매핑해볼 것이니 MoveForWard 이것을 잘 기억해주자

이 때 매핑된 키의 이름과 오브젝트, 콜백함수의 주소를 매개변수로 사용하면된다.

	PlayerInputComponent->BindAxis(FName("MoveForward"), this, &ABird::MoveForward);
void ABird::MoveForward(float Value)
{
	UE_LOG(LogTemp, Warning, TEXT("Value: %f"), Value);
}

지금은 로그를 통해 눌러지냐를 확인해 보았다.

 

 

하지만 이 것은 옛날 인풋시스템 방식으로 이제 Enhanced Input System방식으로 바꿔보자

Pawns폴더에 Input 폴더를 만들어주고 여기에 우클릭해서 입력/입력액션을 추가해주자 

 

그리고 이 입력 액션을 Bird와 이어줄 Input Mapping Context도 추가해주자

 

이 Input Mapping Context를 Bird 폰 클래스와 연결해주려면 일단 블루프린트에서는 다음과 같이 할 수 있다. 

일단 Get Controller와 cast를 통해 폰의 컨트롤러를 가져온다.

 

가져온 컨트롤러를 Enhanced Input 시스템을 통해 연결해주면 된다. 이때 Priority에 따라 여러 콘텍스트가 작동할지 정해진다. 

 

그리고 만들어둔 입력액션을 사용하려면 블루프린트에서 액션이벤트를 가져온 뒤 원하는 흐름을 추가해주면 된다.

 

 

이제 이 과정을 코드로 구현해보자 

일단 필요한 함수와 변수를 가져오기 위해 모듈을 가져오는 부분을 수정해주자

 

Slash.Build.cs

PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore","EnhancedInput" });

 

그리고 입력애션과 입력매핑컨텍스트 변수를 헤더파일에서 전방선언해주고 입력액션에 들어갈 함수도 선언해주자

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
	UInputMappingContext* BirdMappingContext;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
	UInputAction* MoveAction;

	//구조체는 헤더파일 가져와야한다.
	void Move(const FInputActionValue& Value);

 

이제 BeginPlay에서 입력컨텍스트에 클래스를 붙여주자

void ABird::BeginPlay()
{
	Super::BeginPlay();
	
	//조건문안에 넣는 것이 최적화상으로 좋다
	if (APlayerController* PlayerController = Cast<APlayerController>(GetController()))
	{
		if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem< UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
		{
			Subsystem->AddMappingContext(BirdMappingContext, 0);
		}
	}

}

 

또한 입력액션의 콜백함수도 완성해주자

void ABird::Move(const FInputActionValue& Value)
{
	const bool CurrentValue = Value.Get<bool>();
	if (CurrentValue)
	{
		UE_LOG(LogTemp, Warning, TEXT("Good"));
	}
}

이 콜백 함수를 입력액션에 바인딩 해주자

// Called to bind functionality to input
void ABird::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

	if (UEnhancedInputComponent* EnhancedInputComponent = CastChecked<UEnhancedInputComponent>(PlayerInputComponent))
	{
		EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &ABird::Move);
	}

}

 

마지막으로 블루프린트상에서 입력액션과 매핑컨텍스트를 초기화해주면 된다.

 

이제 본격적으로 폰을 움직여보자

 

일단 옛날 인풋방식으로 하는것을 보자

플레이어기준 앞을 향하는 벡터를 가지고와서 그방향으로 AddMovementInput을 해주면 된다.

void ABird::MoveForward(float Value)
{
	if (Controller && (Value != 0.f))
	{
		FVector Forward = GetActorForwardVector();
		AddMovementInput(Forward, Value);
	}
}

 

그리고 실제로 움직일 수 있게 블루프린트상에 FloatingPawnMovement을 추가해주자

움직이는 모습을 볼 수 있다.

이제 이것을 향상된 인풋시스템으로 바꿔보자

입력 액션에서 타입을 float로 바꿔주고 입력 매핑 컨텍스트에 w키도 받을 수 있게 추가해주자 

w키를 눌렀을 때는 w와 반대가 될 수 있도록 Modifiers에 Negate 하나 추가해주자

 

코드상에서 Value를 받아서 앞으로 움직이게 해주자 함수는 동일하게 AddMovementInput를 사용해주자

void ABird::Move(const FInputActionValue& Value)
{
	const float DirectionValue = Value.Get<float>();

	if (Controller && (DirectionValue != 0.f))
	{
		FVector Forward = GetActorForwardVector();
		AddMovementInput(Forward, DirectionValue);
	}
}

 

폰이 움직이는 것을 볼 수 있다.

 

지금상태에서는 어떻게 움직이는지 보이지 않기때문에 카메라를 조정해보도록하자

먼저 블루프린트에서 이 작업을 해보자

첫번째 방법은 카메라 컴포넌트를 루트아래에 두는 것이다. 위치와 각도를 조절하면 탑뷰형태로 볼 수 있다.

 

 

하지만 이런 방식보다는 스프링암을 활용하는 것이 좋다.

 

스프링암은 만약 카메라가 벽과 충돌하게 된다면 줄어들어서 벽 넘어 플레이어를 볼 수 있게 한다.

그리고 팔 길이와 각도도 자유롭게 변경가능하다. 카메라를 돌리는 대신 팔의 각도와 길이를 통해 카메라를 조정하는 효과를 얻는 것이다.

 

이것을 이제 코드로 구현해보자

우선 스프링암과 카메라를 전방선언을 통해 헤더파일에 선언해주고 cpp파일에서 서브오브젝트를 통해 초기화 해주자

Bird.h

	UPROPERTY(VisibleAnywhere)
	USpringArmComponent* SpringArm;

	UPROPERTY(VisibleAnywhere)
	UCameraComponent* ViewCamera;

 

Bird.cpp

SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
SpringArm->SetupAttachment(GetRootComponent());
SpringArm->TargetArmLength = 300.f;

ViewCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("ViewCamera"));
ViewCamera->SetupAttachment(SpringArm);

 

이후 핫리로딩을 해주고 각도를 조금 조정해주면 다음과 같은화면이 나오게된다.

 

이제 앞 뒤뿐만 아니라 원하는 방향으로 움직일 수 있게 해보자 이를 위해 마우스 움직임을 사용할 것이다.

입력 액션을 추가해주자 이때, 2D vector를 받을 수 있게하여 위아래 좌우를 모두 볼 수 있게하자

 

 

입력컨텍스트도 xy를 모두 받을 수 있게 추가해주자

 

이제 코드를 보자면

헤더파일에서는 LookAction에 추가될 액션포인터 변수를 선언해주고 콜백함수도 정의해주자

UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
UInputAction* LookAction;

void Look(const FInputActionValue& Value);

 

cpp파일에서는 선언된 콜백함수를 완성해주고 바인딩해주자

void ABird::Look(const FInputActionValue& Value)
{
	const FVector2D LookAxisValue = Value.Get<FVector2D>();
	if (GetController())
	{
		AddControllerYawInput(LookAxisValue.X);
		AddControllerPitchInput(LookAxisValue.Y);
	}
}

// Called to bind functionality to input
void ABird::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

	if (UEnhancedInputComponent* EnhancedInputComponent = CastChecked<UEnhancedInputComponent>(PlayerInputComponent))
	{
		EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &ABird::Move);
		EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &ABird::Look);
	}

}

 

컴파일해준 뒤 블루프린트의 액션칸에 Look 입력액션을 추가해주고 컨트롤러 회전의 피치와 요를 사용할 수 있게 바꿔준다.

 

결과

 

입력컨텍스트에서 modifier을 조정하여 자연스럽게 날 수 있도록 조정하자

 

폰 클래스는 액터클래스에서 상속받은 기능외에 추가 기능을 가지고 있다. 

그 추가기능 중 중요한 기능은 객체에 입력에 따른 변화를 줄 수 있다는 것이다.

 

Bird 폰 Class를 생성해보자

 

그리고 만든 클래스에 기반한 블루프린트도 만들어주자

 

이제 충돌에 사용할 기본적인 캡슐을 먼저 만들어보자. 

실제 메시를 통한 충돌 계산은 비용이 많이 드는 작업으로 캡슐같은 기본적인 모양으로 충돌을 감지한다. 

상속관계

이제 코드에서 추가해보자

일단 UCapsuleComponent 객체는 언리얼 헤더파일에서 Include를 해주어야하는데 이때 주의해야할 점은

항상 .generated.h이 제일 마지막에 위치해야한다는 것이다. 그렇지 않으면 오류가 발생하게 된다.

이유는 .generated.h파일이 리플랙션 시스템의 통합과 관련되어 있기 때문이다.

하지만 이러한 헤더파일은 각 객체에서 Include 작업을 하는 것은 안좋다. 

이유는 후에 다루기로하자.

 

먼저 UCapsuleComponent 변수를 선언해주고 블루프린트상에 보일 수 있게 하자

이렇게만 했을 때는 그냥 포인터변수를 가지고 있는 것으로

우리는 이것을 실제로 쓰기위해 하위 객체를 통해 초기화 해주어야한다.

Bird.h

private:
	UPROPERTY(VisibleAnywhere)
	UCapsuleComponent* Capsule;

 

Bird.cpp

CreateAbstractDefaultSubobject를 통해 초기화해준다.

	Capsule = CreateAbstractDefaultSubobject<UCapsuleComponent>(TEXT("Capsule"));
	SetRootComponent(Capsule);

 

생성된 모습

 

높이와 너비를 조절하려면 아래의 코드와 같이 사용하면 된다.

ABird::ABird()
{
	PrimaryActorTick.bCanEverTick = true;

	Capsule = CreateAbstractDefaultSubobject<UCapsuleComponent>(TEXT("Capsule"));
	Capsule->SetCapsuleHalfHeight(20.f);
	Capsule->SetCapsuleRadius(15.f);
	SetRootComponent(Capsule);
}

 

★★헤더파일은 각 객체에서 Include 작업을 하는 것은 안좋다. 

이유 : 각 객체의  헤더파일에서 계속 헤더 파일을 참조하게 되면 각 객체를 호출 할때마다 호출할 헤더코드가 가지고 있는 또 다른 헤더파일에 관련된 코드를 호출하게 되기때문에 컴파일 시간이 길어진다.

그래서 이러한 문제점을 해결하기 위해

Forward Declaration- 전방선언을 사용한다.

전방선언은

전방 선언(Forward Declaration)은 컴파일러에게 특정 클래스, 구조체, 함수 등의 존재를 미리 알리는 선언으로 이를 통해 해당 객체가 나중에 정의될 것임을 알려준다. 단순히 컴파일러에게 "이 이름이 존재한다"는 것을 알려주는 역할

헤더파일에서 전방선언을 한 뒤 

정확하게 사용하는 것은 cpp파일에서 헤더파일을 가져온 뒤 사용하게 된다. 

정리하자면 

문제점

  1. 컴파일 시간 증가: 헤더 파일을 여러 번 포함하면 컴파일러가 포함된 모든 헤더 파일을 다시 처리해야 합니다. 이는 특히 대규모 프로젝트에서 컴파일 시간을 크게 증가시킬 수 있습니다.
  2. 순환 종속성: 클래스들이 서로를 참조할 때, 헤더 파일에서 직접 #include를 사용하면 순환 종속성 문제가 발생할 수 있습니다. 이는 컴파일 오류를 초래할 수 있습니다.
  3. 의존성 증가: 불필요한 헤더 파일을 포함하면 파일 간의 의존성이 증가하여 코드 수정 시 많은 파일을 다시 컴파일해야 할 수 있습니다. 이는 코드 유지 보수를 어렵게 만듭니다.
  4. 코드 복잡성: 헤더 파일에 너무 많은 #include가 있으면 코드가 복잡해지고, 어떤 헤더 파일이 실제로 필요한지 파악하기 어려워집니다.

장점

 

  • 컴파일 시간 단축: 헤더 파일을 덜 포함함으로써 컴파일러가 처리해야 할 파일의 양을 줄여 컴파일 시간을 단축할 수 있다
  • 순환 종속성 해결: 클래스들이 서로를 참조하는 경우, 전방 선언을 통해 순환 종속성 문제를 해결할 수 있다
  • 의존성 관리: 파일 간의 의존성을 줄여, 코드 수정 시 재컴파일되는 범위를 최소화할 수 있다
  • 코드 명확성: 헤더 파일을 깔끔하게 유지하고, 필요한 정의만 소스 파일에서 포함하여 코드의 명확성을 높일 수 있다.

 

반드시 포함해야 할 경우:

  1. 부모 클래스에서 상속받을 때
    • 부모 클래스의 멤버 변수와 함수를 사용할 수 있도록 하기 위해 헤더 파일을 포함한다.
  2. 타입의 크기가 필요할 때
    • 클래스의 새로운 인스턴스를 생성해야 하는 경우, 해당 타입의 크기를 알아야 하므로 헤더 파일을 포함한다.
  3. 멤버 변수/함수에 접근할 때
    • 클래스의 멤버 변수나 함수를 사용할 때는 해당 클래스의 정의를 알아야 하므로 헤더 파일을 포함한다.

이러한 경우를 제외하고는, 전방 선언을 사용하여 헤더 파일의 불필요한 포함을 피할 수 있다. 이를 통해 컴파일 시간을 단축하고, 코드의 의존성을 줄일 수 있다.

 

이제 코드를 수정해보자 밑의 코드를 헤더파일에서 지워주자

#include "Components/CapsuleComponent.h"

 

Bird.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Pawn.h"
#include "Bird.generated.h"

class UCapsuleComponent;		//전방선언

UCLASS()
class SLASH_API ABird : public APawn
{
	GENERATED_BODY()

public:
	ABird();
	virtual void Tick(float DeltaTime) override;

	// Called to bind functionality to input
	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

protected:
	virtual void BeginPlay() override;
private:
	UPROPERTY(VisibleAnywhere)
	UCapsuleComponent* Capsule;

};

 

이제 본격적으로 Pawn클래스에 매쉬를 붙여보자 

매쉬에는 static과 skeletal이 있는데 static은 말 그대로 정적인 애니메이션이 불가능한 매쉬이고 skeletal은 뼈대로 구성된 매쉬로 애니메이션이 가능하다.

 

먼저 헤더파일에 변수를 추가해주자 이때 변수는 전방선언으로 선언해주자

class USkeletalMeshComponent;
private:
	UPROPERTY(VisibleAnywhere)
	UCapsuleComponent* Capsule;

	UPROPERTY(VisibleAnywhere)
	USkeletalMeshComponent* BirdMesh;

 

그리고 이제 cpp파일에서 변수에 대한 초기화를 진행해주자

#include "Pawns/Bird.h"
#include "Components/CapsuleComponent.h"
#include "Components/SkeletalMeshComponent.h"

ABird::ABird()
{
	PrimaryActorTick.bCanEverTick = true;

	Capsule = CreateDefaultSubobject<UCapsuleComponent>(TEXT("Capsule"));
	Capsule->SetCapsuleHalfHeight(20.f);
	Capsule->SetCapsuleRadius(15.f);
	SetRootComponent(Capsule);

	BirdMesh = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("BirdMesh"));
	BirdMesh->SetupAttachment(GetRootComponent());
}

 

BirdMesh가 추가되었고 이제 메시에 Sk_Crow를 추가해주었다.

 

그리고 여기에 애니메이션모드를 Use Animation Asset으로 바꿔주고 애님을 붙여주자

 

오늘은 캐릭터를 움직여 볼 것이다.

 

일단 블루프린트의 Set Actor Location이라는 기능부터 알아보자

1.Set Actor Location

Set Actor Location

 

Set Actor Location은 말 그대로 액터의 위치를 정해주는 기능이다. 

그리고 결과사진을 보면 블루프린트가 먼저 작동하고 그다음에 구와 벡터가 표시된다는 것을 알 수 있다.

 

이제 코드로 구현해보자 코드에서는 SetActorLocation을 통해 구현할 수 있다.

	SetActorLocation(FVector(0.f, 0.f, 50.f));

결과

 

2.Set Actor Rotation

Set Actor Rotation은 말 그대로 액터를 각도 회전시키는 함수이다. 

블루프린트에서는 Set Actor Rotation노드를 추가해주는 것으로 구현할 수 있다.

Set Actor Rotation

 

이제 코드로 구현해보자

SetActorRotation 함수와 FRotator변수를 사용하여 회전해주었다. 

FRotator의 각 변수는 피치(Pitch, X축), 요(Yaw, Z축), 롤(Roll, Y축) 순서이다.

	SetActorRotation(FRotator(0.f, 90.f, 0.f));

결과화면

3.Add World Offset & Rotation

 

Add World Offset, Add World Rotation 을 통해월드좌표에서 벡터를 더주고 회전을 시킬 수 있다.

Add World Offset
Add World Rotation

이제 코드로 구현해보자

해당 부분을 구현하기전에 매 프레임마다 구가 이동하는 모습을 볼 수 있게 줄 매크로함수를 만들어주자

#pragma once
#include "DrawDebugHelpers.h"

#define DRAW_SPHERE(Location) if (GetWorld()) DrawDebugSphere(GetWorld(),Location,25.f,12,FColor::Red,true);
#define DRAW_SPHERE_SingleFrame(Location) if (GetWorld()) DrawDebugSphere(GetWorld(),Location,25.f,12,FColor::Red,false,-1.f);
#define DRAW_LINE(StartLocation,EndLocation) if(GetWorld()) DrawDebugLine(World, StartLocation,EndLocation, FColor::Red, true, -1.f, 0, 1.f);
#define DRAW_LINE_SingleFrame(StartLocation,EndLocation) if(GetWorld()) DrawDebugLine(World, StartLocation,EndLocation, FColor::Red, false, -1.f, 0, 1.f);
#define DRAW_POINT(Location) if(GetWorld()) DrawDebugPoint(World, Location, 15.f, FColor::Red, true);
#define DRAW_POINT_SingleFrame(Location) if(GetWorld()) DrawDebugPoint(World, Location, 15.f, FColor::Red, false,-1.f);
#define DRAW_VECTOR(StartLocation,EndLocation) if (GetWorld()) \
	{ \
		DrawDebugLine(World, StartLocation, EndLocation, FColor::Red, true, -1.f, 0, 1.f); \
		DrawDebugPoint(World, EndLocation, 15.f, FColor::Red, true); \
	}
#define DRAW_VECTOR_SingleFrame(StartLocation,EndLocation) if (GetWorld()) \
	{ \
		DrawDebugLine(World, StartLocation, EndLocation, FColor::Red, false, -1.f, 0, 1.f); \
		DrawDebugPoint(World, EndLocation, 15.f, FColor::Red, false,-1.f); \
	}

 

그리고 이제 틱이벤트에 AddActorWorldOffset을 통해 구가 움직이도록 해보자 

// Called every frame
void AItem::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	AddActorWorldOffset(FVector(1.f, 0.f, 0.f));
	DRAW_SPHERE_SingleFrame(GetActorLocation());
}

AddActorWorldOffset

매 프레임마다 움직이고 있는 모습을 볼 수 있다 하지만 프레임의 차이가 있기때문에 움직임속도의 차이가 있을 것이다.

일단 우리는 프레임을 고정시켜주어 일정한 속도를 내도록 해주자

프레임 고정

하지만 프레임단위로 이동하는 방식보다는 어느 환경에서나 동일하게 흘러가는 델타타임을 이용하는 방법이 좋다. 

움직이는 정도에 Delta Time을 곱해주는 것으로 처리하면 된다. 

// Called every frame
void AItem::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
	
	//움직이는 정도 - cm/s
	float MovementRate = 50.f;

	// MovementRate*DeltaTime -> cm/s * s/frame = cm/frame
	AddActorWorldOffset(FVector(MovementRate*DeltaTime, 0.f, 0.f));
	DRAW_SPHERE_SingleFrame(GetActorLocation());
}

결과

그렇게 되면 프레임속도 에 상관없이 50cm씩 움직이는 구를 볼 수 있다. 이제 프레임 고정을 없애주자

 

이제 여기에 회전을 추가해보자 

회전 또한 프레임속도에 독립적으로 수행될 수 있도록 Delta Time을 곱해준다.

회전하는 모습을 볼 수 있도록 벡터그리는 함수를 추가해주자

// Called every frame
void AItem::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
	
	//움직이는 정도 - cm/s
	float MovementRate = 50.f;
	float RotationRate = 45.f;
	
	// MovementRate*DeltaTime -> cm/s * s/frame = cm/frame
	AddActorWorldOffset(FVector(MovementRate*DeltaTime, 0.f, 0.f));
	AddActorWorldRotation(FRotator(0.f, RotationRate*DeltaTime, 0.f));
	DRAW_SPHERE_SingleFrame(GetActorLocation());
	DRAW_VECTOR_SingleFrame(GetActorLocation(), GetActorLocation() + GetActorForwardVector() * 100.f);
}

 

구 회전

 

4. 삼각함수 활용

 

이번에는 삼각함수를 활용해보자 

사인 함수를 활용하여 물체가 사인주기에 맞춰서 위아래로 움직이게 해보자 

블루프린트에서는 Sin(Radius) 노드를 통해 World Offset의 Z좌표를 바꾸어주었다.

Sin

 

이제 코드로 구현해보자 

시간이 지날때마다 늘어나거나 줄어드는 값을 구현하기 위해 float 변수를 추가하고 이를 통해 Sin 함수값을 얻어온다. 이때 Sin 함수는 FMath에 있다.

// Called every frame
void AItem::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
	
	RunningTime += DeltaTime;

	float DeltaZ = 0.25f*FMath::Sin(RunningTime*5.f);

	AddActorWorldOffset(FVector(0.f, 0.f, DeltaZ));

	DRAW_SPHERE_SingleFrame(GetActorLocation());
	DRAW_VECTOR_SingleFrame(GetActorLocation(), GetActorLocation() + GetActorForwardVector() * 100.f);
}

위아래로만 움직이는 모습을 볼 수 있다.

이제 헤더파일의 변수를 선언하여 주기값과 진폭값을 변경할 수 있도록하자

private:
	float RunningTime;
	float Amplitude = 0.25f; //진폭
	float TimeConstant = 5.f;		//x 계수값
};

 

이제 이값을 블루프린팅상에 나타나도록 해보자 

이러한 기능은 UPROPERTY 매크로 함수로 지정해줄 수 있다.

UPROPERTY(EditDefaultsOnly)
float Amplitude = 0.25f; //진폭

 

이렇게 코드를 작성한 뒤 핫리로딩 해주면 아이템 블루프린트의 디테일 패널에 Amplitude 값이 보인다.  이 값을 수정할 수 도 있다.

EditDefaultsOnly 지정값으로 블루프린트 상에서만 수정이 가능하다.

그냥 아이템의 디테일 패널에는 보이지않는 것을 알 수 있다.

만약 EditInstanceOnly로 지정해주게 된다면 기본 디테일창에 보이게 된다.

이 경우에는 블루프린트의 디테일에서는 보이지않게 된다.

UPROPERTY(EditInstanceOnly)
float TimeConstant = 5.f;		//x 계수값

만약 EditAnywhere로 바꿔주게 된다면 둘 다의 디테일창에서 보이게 되고 수정할 수 있다. 

	UPROPERTY(EditAnywhere)
	float Amplitude = 0.25f; //진폭

	UPROPERTY(EditAnywhere)
	float TimeConstant = 5.f;		//x 계수값

 

그리고 보이긴하지만 편집할 수 없게 만들 수 도 있다.

UPROPERTY(VisibleDefaultsOnly);			//블루프린트
UPROPERTY(VisibleInstanceOnly);			//인스턴스상
UPROPERTY(VisibleAnywhere);				//둘다
float RunningTime;

VisibleAnywhere

이제 이 변수를 이벤트그래프에서 사용할 수 있도록 만들어보자

이때 BlueprintReadOnly라는 매개변수를 넣어주면 되는데 이때 Private로 선언된 변수에서는 작동하지않는다.

UPROPERTY(EditAnywhere,BlueprintReadOnly)

 

이렇게 설정해주면 블루프린트 상에서 가져와 쓸 수 있다. 지금은 ReadOnly라 Get만 쓸 수 있다

 

이것을 BlueprintReadWrite로 바꿔주게 되면 Set도 사용할 수 있게 된다.

또한 카테고리를 지정해 줄 수 있다.

UPROPERTY(EditAnywhere, BlueprintReadWrite,Category="Sine Parameters")
float Amplitude = 0.25f; //진폭

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Sine Parameters")
float TimeConstant = 5.f;		//x 계수값

 

프라이버으로 지정된 변수를 가져오고 싶을 때는 meta=(AllowPrivateAccess="true")로 지정하고 만들어 주면 된다.

UPROPERTY(VisibleAnywhere,BlueprintReadOnly,meta=(AllowPrivateAccess="true"));
float RunningTime;

 

 

변수뿐만 아니라 함수또한 이런방식으로 보이게 할 수 있다. 

함수는 UFUNCTION(BlueprintCallable)을 통해 블루프린트 상에 보이게 할 수 있다.

	UFUNCTION(BlueprintCallable)
	float TransformedSin(float Value);

 

만약 이것을 UFUNCTION(BlueprintPure) 으로 지정한다면 Get Actor Location과 같이 계산하는 값을 반환해주는 용도로 사용할 수 있다.

	UFUNCTION(BlueprintPure)
	float TransformedSin(float Value);

 

 

★ ★ ★ 제네릭 함수 - 템플릿 문법

 

C++의 템플릿 문법은 제네릭 프로그래밍을 가능하게 하여, 클래스나 함수를 작성할 때 특정 데이터 타입에 의존하지 않고, 다양한 데이터 타입을 처리할 수 있도록 한다.

사용하는 방법은 함수위에 template<typename T>을 선언하고 밑에 typename으로 선언한 제네릭 변수를 통해 함수를 구성해주면 된다. 

template<typename T>
inline T AItem::Avg(T First, T Second)
{
	return (First + Second) / 2;
}

 

사용할 때는 아래와 같이 사용해주면 된다.

Avg<int32>(1, 3);
UE_LOG(LogTemp, Warning, TEXT("Avg : %d"), Avg<int32>(1, 3));

 

이제 액터에 스태틱매쉬를 붙여 게임화면에 보일 수 있도록하자.
블루프린트에서 스태틱매쉬를 추가한 뒤 Sphere을 추가해주자 

 

이 것을 코드로 구현해 보겠다. 

일단 코드 상에서 인스턴스를 건드리려면 아직 액터들이 모두 생성되지않은 생성자 단계가 아니라 BeginPlay이후에 이루어져야한다. 

또한 이 컴포넌트의 하위 객체를 이용할 때는 템플릿 함수를 사용하여 지정해주어야한다.

이 함수는 포인터를 반환해준다.

 

그럼 이제 매쉬를 저장해줄 포인터 변수를 선언해주자

	UStaticMeshComponent* ItemMesh;

 

그리고 생성자에서 매쉬를 붙여주자

AItem::AItem()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

	ItemMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("ItemMeshComponent"));
	RootComponent = ItemMesh;
}

 

'게임공부 > Unreal Engine' 카테고리의 다른 글

[Unreal Engine][C++]6.움직이기  (1) 2024.07.16
[Unreal Engine][C++]5.Pawn 클래스  (0) 2024.07.15
[Unreal Engine][C++]3. 디버그  (0) 2024.07.10
[Unreal Engine][C++]2. C++살펴보기  (0) 2024.07.09
[Unreal Engine][C++]1.시작  (0) 2024.07.08

 

 

오늘은 게임 플레이내에서 구나 선, 점을 통해 매쉬가 없이도 화면에 나타날 수 있게 해보자

 

1. 디버그 구 그리기

일단 블루프린트상에서는 Draw Debug Sphere을 추가하여 만들 수 있다.

BluePrints

 

이렇게하면 0,0,0에 구가 나오게 된다. 

결과

 

이제 액터위치에서 구가 보이도록 만들어주자 

BluePrints
결과

 

이제 이걸 C++로 구현해보자 

구현하려면 일단 DrawDebugHelpers 헤더파일을 추가해줘야한다.

DrawDebugSphere 함수를 통해 그려주면 되는데 이 함수의 매개변수에는 UWorld와 FVector가 들어가는데 이때  

GetWorld GetActorLocation을 통해 값을 전달해주면 된다.

	UWorld* World = GetWorld();

	if (World)
	{
		FVector Location = GetActorLocation();
		DrawDebugSphere(World, Location, 25.f, 24, FColor::Red, false, 30.f);
	}

 

결과

 

이것을 매크로 함수를 통해 구현해보자 

#define THIRTY 30
#define DRAW_SPHERE(Location) if (GetWorld()) DrawDebugSphere(GetWorld(),Location,25.f,12,FColor::Red,true);

 

	FVector Location = GetActorLocation();

	DRAW_SPHERE(Location);

 

결과

 

이 매크로 함수는 Slash.h에 넣어서 모든 클래스에서 사용가능하도록 만들어주자 

 

Slash.h

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "CoreMinimal.h"

#define DRAW_SPHERE(Location) if (GetWorld()) DrawDebugSphere(GetWorld(),Location,25.f,12,FColor::Red,true);
#include "Slash\Slash.h"

 

동일하게 동작하는 것을 볼 수 있다. 

결과화면

 

2. 디버그 선 그리기

블루 프린트에서는 Draw Debug Line을 사용하여 선을 그릴 수 있다.

 

 

결과

이제 이것을 코드로 구현해보자

 

C++에서는 DrawDebugLine으로 선을 그릴 수 있다.

if (World)
{
	FVector Forward = GetActorForwardVector();
	DrawDebugLine(World, Location, Location + Forward * 100.f, FColor::Red, true, -1.f, 0, 1.f);
}

 

결과

이제 이것을 매크로로 만들어 간결하게 쓸 수 있도록 만들어 보자

 

#define DRAW_LINE(StartLocation,EndLocation) if(GetWorld()) DrawDebugLine(World, StartLocation,EndLocation, FColor::Red, true, -1.f, 0, 1.f);

 

	DRAW_LINE(Location, Location+Forward * 100.f);

 

결과

 

3.디버그 점 그리기

 

블루 프린트에서는 Draw Debug Point를 통해 그릴 수 있다.

 

결과

디버그 포인트는 크기가 어느곳에서 보던 일정하다 하지만 구나 선은 멀어질수록 작아진다

 

이제 이것을 코드로 구현해보자

코드에서는 DrawDebugPoint함수를 사용한다.

	if (World)
	{
		DrawDebugPoint(World, Location + Forward * 100.f, 15, FColor::Red, true);
	}

결과

 

이제 편하게 사용할 수 있도록 매크로 함수로 만들어주자

#define DRAW_POINT(Location) if(GetWorld()) DrawDebugPoint(World, Location, 15.f, FColor::Red, true);
	DRAW_POINT(Location + Forward * 100.f);

결과

추가적으로 점과 선을 한번에 그려주는 즉 벡터를 그려주는 매크로 함수를 만들어보자 벡터는 이전에 만들었던 라인과 점을 그리는 것을 통합하여 만들어주자

#define DRAW_VECTOR(StartLocation,EndLocation) if (GetWorld()) \
	{ \
		DrawDebugLine(World, StartLocation, EndLocation, FColor::Red, true, -1.f, 0, 1.f); \
		DrawDebugPoint(World, EndLocation, 15.f, FColor::Red, true); \
	}
	/*DRAW_LINE(Location, Location + Forward * 100.f);
	DRAW_POINT(Location + Forward * 100.f);*/

	DRAW_VECTOR(Location, Location + Forward * 100.f);

결과

 

이제 매크로 함수를 모아둔 헤더파일을 하나 만들어주자

 

Source/Slash/

경로에 넣어주자

DebugMacros.h

#pragma once
#include "DrawDebugHelpers.h"

#define DRAW_SPHERE(Location) if (GetWorld()) DrawDebugSphere(GetWorld(),Location,25.f,12,FColor::Red,true);
#define DRAW_LINE(StartLocation,EndLocation) if(GetWorld()) DrawDebugLine(World, StartLocation,EndLocation, FColor::Red, true, -1.f, 0, 1.f);
#define DRAW_POINT(Location) if(GetWorld()) DrawDebugPoint(World, Location, 15.f, FColor::Red, true);
#define DRAW_VECTOR(StartLocation,EndLocation) if (GetWorld()) \
	{ \
		DrawDebugLine(World, StartLocation, EndLocation, FColor::Red, true, -1.f, 0, 1.f); \
		DrawDebugPoint(World, EndLocation, 15.f, FColor::Red, true); \
	}

 

결과

 

디버깅 메세지 보기

 

로그는 Output 로그창에 뜨고 Print는 화면에 뜬다.

 

 

Key 값이 None 일때는 쌓이지만 Key가 같으면 제일 오른쪽 즉 제일 최근에 실행된걸로 나온다.

 

틱은 매 프레임마다 실행되는 이벤트이다. 

 

 

프레임의 Delta Seconds가 Float라 String으로 변환하는 노드가 추가된걸 볼 수 있다.

 

이걸 이제 C++로 구현해보자 

// Called when the game starts or when spawned
void AItem::BeginPlay()
{
	Super::BeginPlay();
	
	UE_LOG(LogTemp,Warning,TEXT("Begin Play called!"));

	if (GEngine) {
		GEngine->AddOnScreenDebugMessage(1, 60.f, FColor::Cyan, FString("Item OnScreen!!!!"));
	}

}

 

보면 GEngine의 AddOnScreenDebugMessage함수로 구현할 수 있는 것을 알 수 있다.

On Screen

 

Tick 이벤트 또한 코드를 통해 구현 할 수 있다

// Called every frame
void AItem::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	UE_LOG(LogTemp, Warning, TEXT("Delta Time: %f"), DeltaTime);
}

Output Log

 

이제 화면에 띄워보자 

이때 AddOnScreenDebugMessage 함수를 사용하면 된다. 하지만 이때 DeltaTime 부분을 FString 으로 바꿔줘야한다. 

// Called every frame
void AItem::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	UE_LOG(LogTemp, Warning, TEXT("Delta Time: %f"), DeltaTime);

	if (GEngine) {

		FString Message = FString::Printf(TEXT("Delta Time: %f"), DeltaTime);
		GEngine->AddOnScreenDebugMessage(1, 60.f, FColor::Cyan, Message);
	}
}

 

이렇게하면 Log에서 출력해주던 부분이 FString으로 바꿔줘서 Screen에 보여지 된다.

OnScreen

액터의 이름을 출력하고 싶을 때 GetName() 함수를 사용해서 FString으로 받아오게 되는데 이때는 Printf에서

*Name으로 붙여줘야 사용가능한데 이때 *은 포인터를 쓸 때 사용하는 *가 아니고 FString 클래스 자체에 정의된

연산자이다.

이때 *가 하는 일은 C스타일의 문자를 반환한다.

 

	if (GEngine) {

		FString Name = GetName();
		FString Message = FString::Printf(TEXT("Delta Time: %s"), *Name);
		GEngine->AddOnScreenDebugMessage(1, 60.f, FColor::Cyan, Message);
	}

OnScreen

 

 

이제 본격적으로  C++를 통해 언리얼을 다뤄보자 

 

일단 툴/C++클래스추가 에서 액터추가에서 C++ 클래스를 만들어주자 

 

 

기본 코드를 살펴보자면 

Item.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Item.generated.h"

UCLASS()
class SLASH_API AItem : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	AItem();

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;

};

 

리플랙션 코드와 액터의 기능을 쓸 수 있게하는 헤더 파일들과 AActor를 상속받고 있는 모습을 볼 수 있다. 

Item.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "Items/Item.h"

// Sets default values
AItem::AItem()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;		//매틱마다 실행되도록

}

// Called when the game starts or when spawned
void AItem::BeginPlay()
{
	Super::BeginPlay();
	
}

// Called every frame
void AItem::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}

 

Actor 쪽에서 무언가 구현할 것을 대비하여 부모쪽의 BeginPlay를 실행하기위해  Super::로 실행한다. 

 

이제 만든 클래스를 바탕으로 블루프린트를 만들어보자 

모든 액터는 초기에 DefaultSceneRoot라는 Component를 가지고 있다.

 

모든 블루프린트에는 이벤트 그래프가 있는데 이것은 블루프린트를 통해 코드의 흐름을 작성하고 실행시킬 수 있는 도구이다. 

예를 들면 Begin Play 이벤트 즉 시작할 때 String을 화면에 띄우는 기능을 블루프린트를 통해 구현할 수 있는 것이다.

그리고 다음은 Construction Script이다

이것은 게임이 시작되기 전에 실행되며 블루프린트의 속성이 바뀔 때 실행된다.

 

그리고 만약 이 함수가 실행되는 곳, 즉 액터와 너무 멀리있다면 실행결과가 화면에 보이지 않는다 

 

 

 

이제 이것을 C++를 통해 구현해보자

// Called when the game starts or when spawned
void AItem::BeginPlay()
{
	Super::BeginPlay();
	
	UE_LOG(LogTemp,Warning,TEXT("Begin Play called!"));
}

 

 

중요한점 

 

핫리로딩 - 라이브컴파일을 하려면  밑의 버튼을 누르면 된다.

비쥬얼 스튜디오에서 ctrl shift b 를 누르면 컴파일이 된다.

 

 

https://www.udemy.com/course/unreal-engine-5-the-ultimate-game-developer-course/

 

 

언리얼 엔진에서 Large World Open World

 

Large World는 하나의 맵에서 다른 맵으로 이동하면 기존의 맵을 언로드하고 다른 맵을 로드하는 방식으로 작동한다.

 

Open World는 하나의 큰맵에서 섹션을 나누고 플레이어가 있는 섹션만 로드한다. -> 월드 파티션 

언리얼에서 제공해주는 레벨 생성에에는오픈월드가 있다.

오픈월드 상의 미니맵

 

맵의 구성요소를 살펴보자 

1. 하늘 

하늘을 구성하는 요소에는

●Sky AtmosPhere(대기) 

실제 지구의 대기처럼 빛을 산란시켜 준다. 두개의 다른 광원을 통해 달과 태양을 구현할 수도 아니면 2개의 태양을 구현할 수 도 있다.

● Directional Light(지향성 광원)

한방향으로 나아가는 빛으로 보통 태양빛으로 사용된다. 

대기와 지향성광원 추가화면
광원 2개

이때 라이트의 기동성은 

static 일때는 위치나 방향,강도 색깔도 바꿀 수 없지마 연산이 빠르다

Stationary - 색, 강도 변경가능하지만 위치, 회전 변경불가 정적인 물체에 빛을 비춰 그림자생성

Movable - 동적 그림자생성, 가장 비싸지만  현실감\

 

●Sky Light - 먼거리의 부분을 포착, 게임전체에 균일한 조명 실시간 캡처가능

안개와 구름 

● Exponential height Fog

안개 효과는 높이에 따라 안개의 밀도가 기하급수적으로 증가하는 특성을 가지고 있다.

● Volumetric Clouds

동적 구름 

Fog와 Clouds 적용화면

그 다음 온도와 회전을 달리주면 이렇게 된다. 

결과화면

 

풍경- 땅 만들기

LandScape는 여러 매쉬가 펼처저 있는 것으로 이것을 수정하고 조작하는 것으로 여러 땅모양을 만들 수 있다.

랜드스케이프 모드로 들어간 후 32 x 32 짜리의 Landscape를 만들어주자

LandScape 만든 결과

 

조각과 침식등을 추가해 사막처럼 만들어보았다.

 

이후Material을 추가해주었다. 

Material은 새로운 Material을 만든 뒤 여러 Material을 조합해서 만들어주었다.

만든 Material을 LandScape 머티리얼로 넣어주고 페인트의 레이어에서 +한뒤 Weight 로 선택해서 만들어주면 된다.

 

결과화면

https://www.youtube.com/watch?v=ljmOsZVrtok&list=PLiSlOaRBfgkcPAhYpGps16PT_9f28amXi&index=22

오늘은 AI가 플레이어를 감지하고 쫓아 다닐 수 있도록 만들어보자 

 

일단 AI_Controller에 컴포넌트를 추가해서 플레이어를 감지할 수 있도록 AIPerception 컴포넌트를 추가해준다.

이 컴포넌트에서 감지환경설정을 시야구성으로 바꿔준다.

이때 센스는 기본을 따르도록 하고 추후과정에서 이 값을 조절하기로 하자.

AIPerception 컴포넌트

귀속감지는 블루프린트 상의 오류방지를 위해 모두 체크해주자

 

타킷 퍼셉션 업데이트 시 이벤트 구성

 

블랙보드에 키를 추가해서 만약 감지했을 때 Target Actor이면 따라가고 아니면 돌아다니는 과정을 수행하도록 한다.

 

이제 AI 블랙보드 트리에서 Chase Target Sequence를 만들어주고 새로운 테스크를 만들어주자 

 

블랙보드 키를 가져와서 액터에 넣어주고 이를 따라가게 하자 멈추는 거리는 100정도로 설정해주자 

 

이후 블랙보드트리에서 

왼쪽에 Chase를 붙여서 이게 먼저 실행될 수 있도록 하며 BlackBoard값에 따라 해당 테스크가 수행되도록 데코레이터를 추가해준다.

전체 트리 및 Chase쪽 데코레이터 설정
Patrol쪽 데코레이터 설정

 

하지만 아직 플레이어를 감지하지 못하고 있다. 이 부분은 플레이어 쪽에 감지 자극소스를 부착해주면된다.

 

그리고 프로젝트 루트폴더에서 Config/DefaultGame.ini 파일을 수정해준다.

https://www.youtube.com/watch?v=UeG9RAVE8sE&list=PLiSlOaRBfgkcPAhYpGps16PT_9f28amXi&index=21

 

오늘은 적이 가지게 될 AI행동 기초를 만들어 볼 것이다.

 

일단 AI폴더를 만들어주고 안에

여러 npc의 기본 바탕이 될 Black Board와 Behavior Tree, AIController 블루프린트 크래스를 만들어준다. 

 AIController 클래스에서는 시작될 때 , Run Behavior Tree를 실행시켜 만들어둔 Behavior Tree가 작동하도록 한

다. 

 

전에 만들어둔 더미캐릭터를 AI폴더로 가지고 온다. 

그리고 디테일 창에 ai를 검색한 후 폰부분을 바꿔주면 된다.

Place or Spawned가 오류를 줄일 수 있다. 

 

이제 본격적인 흐름도를 만들어보자 이건 트리상에 만들 것인데 Selector와 Seqeunce를 통해 트리구조를 만들고 

새 테스크를 Task폴더에 만들어주자 이번 Task는 Patrol Random Point이다. 

이 테스크는 AI플레이가 시작될 때 호출되는 Receive Execute AI라는 이벤트를 오버라이드 해주는 것으로 동작하도록 만든다. 

 

radius 안의 원의 랜덤한 위치로 움직일 수 있도록 만들어본다. 

그리고 Tree안에 Task를 추가해준다. 

 

그리고 NavMeshBoundsVolume을 통해 AI가 갈 수 있는 길을 표시해준다. 

->이것을 통해 AI가 움직일 수 있다. 

P키를 누르면 AI가 갈 수 있는 모든 길을 보여준다. 

 

이제 랜덤한 위치로 AI가 이동한다.

 

움직이는 AI

 

이제 AI가 자연스럽게 움직이도록 만들어보자 

애니메이션 블루프린트를 만들고 이전에 만들어둔 플레이어의 애니메이션 블루프린터를 참고하자. 

-> 스테이트를 통해 관리되고 있다.

캐릭터 애니메이션 블루프린트의 Locomotion을 복사해서 DefaultSlot에서 작동하도록 만들어 주자

그다음 AI의 매쉬에서 애니메이션 클래스를 변경해준다. 

 

그리고 이벤트 그래프에 속도를 조절하는 부분을 플레이어 부분에서 가져와서 붙여주면 움직이는 모습을 볼 수 있다.

움직이게 하기

 

https://www.youtube.com/watch?v=-5jCh22feJg&list=PLiSlOaRBfgkcPAhYpGps16PT_9f28amXi&index=20

1. 인벤토리 카테고리에 해당하는 텍스트 만들어주기 

각 칸의 카테고리에 해당하는 텍스트를 옆에 붙여준다.

 

2. 활, 방패같은 장비는 등에 먼저 보이도록 로직 수정 

일단 활과 방패의 fbx 파일을 import 해주자 

https://sketchfab.com/3d-models/medieval-shield-b98b8f64d935415aab0fe9b70074511f#download

 

Medieval Shield - Download Free 3D model by Artem Mykhailov - Sketchfab

Artstation post: https://www.artstation.com/artwork/e012eY Based on the concept by Artyom Vlaskin: https://artyomvlaskin.cgsociety.org/okkw/928308

sketchfab.com

https://sketchfab.com/3d-models/wooden-bow-free-a762abcffc27478caf19dfaac086485d#download

 

Wooden Bow Free - Download Free 3D model by Red_Ilya - Sketchfab

Not the strongest bow, but when there is nothing at hand, it is the best

sketchfab.com

 

Fbx파일을 다운 받은 뒤 import 해주고 텍스처 설정을 해줘야한다. 

Shield 텍스처 설정
Bow 텍스처 설정

 

캐릭터 매쉬 골격에 소캣 추가해주기

등에 활과 방패가 들어올 수 있도록 소캣을 추가해준다. 

소캣
결과

이제 실제 매쉬에 스태틱 매쉬 컴포넌트를 추가해서 실제로 적용될 수 있도록 한다. 

스태틱매쉬를 만든 뒤 부모소캣을 위에서 만들어둔 소캣으로 적용하면 된다.

 

소캣 설정

 

이제 만든 매쉬에 들어갈 수  있도록 커스텀 이벤트를 만들어주자 

그리고 들어오는 아이템 타입에 따라 다른 이벤트가 호출되도록 스위치문을 구성해준다. 현재 아머는 없어서 그냥 스트링만 출력하도록 했다.

이벤트 구성 및 흐름도

 

DB 테이블에 방패를 추가해주고 스태틱매쉬와 소캣을 맞게 수정해준다.

DB테이블

 

결과화면

 

+ Recent posts