4.Shader 

이제 Shader부분을 클래스로 만들어주자

일단 기본적인 Vertex와 Pixel 쉐이더를 가지고 있을 Shader 클래스를 만들어주자 

Shader.h

#pragma once

//쉐이더에서 리소스를 사용할 때 어디서사용할지 지칭하는
enum ShaderScope
{
	//비트 플래그용도
	SS_None = 0,
	SS_VertexShader = (1 << 0),
	SS_PixelShader = (1 << 1),
	SS_Both= SS_VertexShader | SS_PixelShader,
};

//쉐이더 공용부
class Shader
{
public:
	Shader(ComPtr<ID3D11Device> device);
	virtual ~Shader();

	virtual void Create(const wstring& path, const string& name, const string& version) abstract;

	ComPtr<ID3DBlob> GetBlob() { return _blob; }

protected:
	void LoadShaderFromFile(const wstring& path, const string& name, const string& version);
protected:
	wstring _path;
	string _name;
	ComPtr<ID3D11Device> _device;
	ComPtr<ID3DBlob> _blob = nullptr;
};

class VertexShader : public Shader
{
	//부모클래스지정
	using Super = Shader;
public:
	VertexShader(ComPtr<ID3D11Device> device);
	~VertexShader();
	//VS
	ComPtr<ID3D11VertexShader> GetComPtr() { return _vertexShader; }

	virtual void Create(const wstring& path, const string& name, const string& version) override;

protected:
	ComPtr<ID3D11VertexShader> _vertexShader = nullptr;
};

class PixelShader : public Shader
{
	//부모클래스지정
	using Super = Shader;
public:
	PixelShader(ComPtr<ID3D11Device> device);
	~PixelShader();
	//PS
	ComPtr<ID3D11PixelShader> GetComPtr() { return _pixelShader; }

	virtual void Create(const wstring& path, const string& name, const string& version) override;

protected:
	ComPtr<ID3D11PixelShader> _pixelShader = nullptr;
};

 

Shader.cpp

#include "pch.h"
#include "Shader.h"

Shader::Shader(ComPtr<ID3D11Device> device)
	:_device(device)
{

}

Shader::~Shader()
{
}

/// <summary>
/// 쉐이더 로딩하는 함수 
/// </summary>
/// <param name="path"></param>
/// <param name="name"></param>
/// <param name="version"></param>
/// <param name="blob"></param>
//쉐이더 파일을 건네주면 경로에 따라 로드해서 빌드한 결과물을 블롭에 전달
void Shader::LoadShaderFromFile(const wstring& path, const string& name, const string& version)
{
	_path = path;
	_name = name;

	const uint32 compileFlag = D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;		//디버그 최적화 건너뛰기

	HRESULT hr = ::D3DCompileFromFile(
		path.c_str(),
		nullptr,
		D3D_COMPILE_STANDARD_FILE_INCLUDE,
		name.c_str(),
		version.c_str(),
		compileFlag,
		0,
		_blob.GetAddressOf(),
		nullptr
	);

	CHECK(hr);
}

VertexShader::VertexShader(ComPtr<ID3D11Device> device) : Super(device) 
{
}

VertexShader::~VertexShader()
{
}

void VertexShader::Create(const wstring& path, const string& name, const string& version)
{
	LoadShaderFromFile(path, name, version);
	HRESULT hr = _device->CreateVertexShader(_blob->GetBufferPointer(),
		_blob->GetBufferSize(), nullptr, _vertexShader.GetAddressOf());
	CHECK(hr);
}

PixelShader::PixelShader(ComPtr<ID3D11Device> device) : Super(device)
{
}

PixelShader::~PixelShader()
{
}

void PixelShader::Create(const wstring& path, const string& name, const string& version)
{
	LoadShaderFromFile(path, name, version);
	HRESULT hr = _device->CreatePixelShader(_blob->GetBufferPointer(),
		_blob->GetBufferSize(), nullptr, _pixelShader.GetAddressOf());
	CHECK(hr);
}

 

이에 맞게 Game클래스도 수정해주자

블롭객체는 각 쉐이더에서 가지고 있기때문에 지워주고 한줄자리 코드들은 위로 옮겨주자

Game.h

	//VS
	shared_ptr<VertexShader> _vertexShader;

	//RS
	ComPtr<ID3D11RasterizerState> _rasterizerState = nullptr;

	//PS
	shared_ptr<PixelShader> _pixelShader;

 

Game.cpp

	_vertexShader = make_shared<VertexShader>(_graphics->GetDevice());
	_pixelShader = make_shared<PixelShader>(_graphics->GetDevice()); 
    
    //정점정보
GeometryHelper::CreateRectangle(_geometry);

//정점버퍼
_vertexBuffer->Create(_geometry->GetVertices());

//IndexBuffer
_indexBuffer->Create(_geometry->GetIndices());
_vertexShader->Create(L"Default.hlsl", "VS", "vs_5_0");

//인풋레이아웃
/// <summary>
/// 입력이 어떻게 이뤄져있는지
/// </summary>
_inputLayout->Create(VertexTextureData::descs, _vertexShader->GetBlob());

_pixelShader->Create(L"Default.hlsl", "PS", "ps_5_0");

 

 

이제 쉐이더에 추가해서 정보를 지정해주고 cpu와 gpu사이의 통신창구를 만들어줘서 SRT 연산을 가능하게 하는

상수버퍼를 클래스로 만들어주자

그리고 주의해야할점이 템플릿클래스는 cpp파일에 구현부를 만들면 안된다! 라는 것이다

그리고 상수버퍼에는 deviceContext도 사용하기 때문에 생성자에 deviceContext도 받아주게 하자

ConstantBuffer.h

#pragma once

//템플릿클래스는 cpp파일에 구현부를 만들면 안된다!
template<typename T>
class ConstantBuffer
{
public:
	ConstantBuffer(ComPtr<ID3D11Device> device, ComPtr<ID3D11DeviceContext> deviceContext)
		:_device(device),_deviceContext(deviceContext)
	{

	}

	~ConstantBuffer(){}

	ComPtr<ID3D11Buffer> GetComPtr() { return _constantBuffer; }

	//상수버퍼 - cpu메모리 -> gpu 복사
	//1단계 버퍼만들기 
	void Create()
	{
		D3D11_BUFFER_DESC desc;
		ZeroMemory(&desc, sizeof(desc));
		desc.Usage = D3D11_USAGE_DYNAMIC;		//CPU Write + GPU Read
		desc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
		desc.ByteWidth = sizeof(T);
		desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;		//매프레임마다 gpu에 고속복사

		HRESULT hr = _device->CreateBuffer(&desc, nullptr, _constantBuffer.GetAddressOf());
		CHECK(hr);
	}

	void CopyData(const T& data)
	{
		D3D11_MAPPED_SUBRESOURCE subResouce;
		ZeroMemory(&subResouce, sizeof(subResouce));

		//맵을 통해 값을 넣어줄 준비를 한다. 이후 값을 넣고(복사해주고) UNMAP
		_deviceContext->Map(_constantBuffer.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &subResouce);
		::memcpy(subResouce.pData, &data, sizeof(data));			//바로 cpu-> gpu 넘어가게 된다.
		_deviceContext->Unmap(_constantBuffer.Get(), 0);
	}
private:
	ComPtr<ID3D11Device> _device;
	ComPtr<ID3D11DeviceContext> _deviceContext;
	ComPtr<ID3D11Buffer> _constantBuffer;
};

 

이에 맞게 Game클래스도 바꿔주자

Game.h

	shared_ptr<ConstantBuffer<TransformData>> _constantBuffer;

 

Game.cpp

_constantBuffer = make_shared<ConstantBuffer<TransformData>>(_graphics->GetDevice(), _graphics->GetDeviceContext());

void Game::Update()
{
	//크기 회전 이동
	Matrix matScale = Matrix::CreateScale(_localScale/3);
	Matrix matRotation = Matrix::CreateRotationX(_localRotation.x);
	matRotation *= Matrix::CreateRotationY(_localRotation.y);
	matRotation *= Matrix::CreateRotationZ(_localRotation.z);
	Matrix matTranslation = Matrix::CreateTranslation(_localposition);

	Matrix matWorld = matScale * matRotation * matTranslation;	//SRT
	_transformData.matWorld = matWorld;

	_constantBuffer->CopyData(_transformData);
}

_deviceContext->VSSetConstantBuffers(0, 1, _constantBuffer->GetComPtr().GetAddressOf());

 

그리고 마지막으로 쉐이더리소스뷰 즉 각 텍스처에 관한 코드를 클래스로 만들어주자

Texture.h

#pragma once

//SRV - 이미지를 어떻게 쓸것인가 - 텍스처
class Texture
{
public:
	Texture(ComPtr<ID3D11Device> device);
	~Texture();

	ComPtr<ID3D11ShaderResourceView> GetComPtr() { return _shaderResourceView; }

	void Create(const wstring& path);

private:
	ComPtr<ID3D11Device> _device;
	ComPtr<ID3D11ShaderResourceView> _shaderResourceView;
};

 

Texture.cpp

#include "pch.h"
#include "Texture.h"

Texture::Texture(ComPtr<ID3D11Device> device) 
	:_device(device)
{
}

Texture::~Texture()
{
}

void Texture::Create(const wstring& path)
{
	DirectX::TexMetadata md;
	DirectX::ScratchImage img;
	HRESULT hr = ::LoadFromWICFile(path.c_str(), WIC_FLAGS_NONE, &md, img);
	CHECK(hr);

	//쉐이더리소스뷰 만들기
	hr = ::CreateShaderResourceView(_device.Get(), img.GetImages(), img.GetImageCount(), md, _shaderResourceView.GetAddressOf());
	CHECK(hr);
}

 

이에 맞게 Game 클래스 코드도 수정해주자

Game.h

	//SRV - 이미지를 어떻게 쓸것인가 - 텍스처
	shared_ptr<Texture> _texture1;

 

Game.cpp

	_texture1 = make_shared<Texture>(_graphics->GetDevice());
    
    	/// <summary>
	/// 쉐이더 리소스 뷰 
	/// </summary>
	_texture1->Create(L"cat.png");

 

동일한 결과가 나온다 

 

5.레스터라이저와 State

이제 레스터라이저와 남아있는 Comptr을 클래스로 만들어주자 

RasterizerState.h

#pragma once
class RasterizerState
{
public:
	RasterizerState(ComPtr<ID3D11Device> device);
	~RasterizerState();

	ComPtr<ID3D11RasterizerState> GetComPtr() { return _rasterizerState; }

	void Create();
private:
	ComPtr<ID3D11Device> _device;
	ComPtr<ID3D11RasterizerState> _rasterizerState;
};

RasterizerState.cpp

#include "pch.h"
#include "RasterizerState.h"

RasterizerState::RasterizerState(ComPtr<ID3D11Device> device)
	:_device(device)
{
	
}

RasterizerState::~RasterizerState()
{
}

void RasterizerState::Create()
{
	D3D11_RASTERIZER_DESC desc;
	ZeroMemory(&desc, sizeof(desc));
	//기본 솔리드 / 백
	desc.FillMode = D3D11_FILL_SOLID;
	desc.CullMode = D3D11_CULL_BACK;
	desc.FrontCounterClockwise = false;

	HRESULT hr = _device->CreateRasterizerState(&desc, _rasterizerState.GetAddressOf());
	CHECK(hr);
}

 

SamplerState.h

#pragma once
class SamplerState
{
public:
	SamplerState(ComPtr<ID3D11Device> device);
	~SamplerState();
	ComPtr<ID3D11SamplerState> GetComPtr() { return _samplerState; }

	void Create();
private:
	ComPtr<ID3D11Device> _device;
	ComPtr<ID3D11SamplerState> _samplerState;
};

SamplerState.cpp

#include "pch.h"
#include "SamplerState.h"

SamplerState::SamplerState(ComPtr<ID3D11Device> device)
	:_device(device)
{
}

SamplerState::~SamplerState()
{
}

void SamplerState::Create()
{
	D3D11_SAMPLER_DESC desc;
	ZeroMemory(&desc, sizeof(desc));
	desc.AddressU = D3D11_TEXTURE_ADDRESS_BORDER;
	desc.AddressV = D3D11_TEXTURE_ADDRESS_BORDER;
	desc.AddressW = D3D11_TEXTURE_ADDRESS_BORDER;
	//순서대로 RGBA
	desc.BorderColor[0] = 1;
	desc.BorderColor[1] = 0;
	desc.BorderColor[2] = 0;
	desc.BorderColor[3] = 1;
	desc.ComparisonFunc = D3D11_COMPARISON_ALWAYS;
	desc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;
	desc.MaxAnisotropy = 16;
	desc.MaxLOD = FLT_MAX;
	desc.MinLOD = FLT_MIN;
	desc.MipLODBias = 0.0f;

	HRESULT hr = _device->CreateSamplerState(&desc, _samplerState.GetAddressOf());
	CHECK(hr);
}

 

BlendState.h

#pragma once
class BlendState
{
public:
	BlendState(ComPtr<ID3D11Device> device);
	~BlendState();

	const float* GetBlendFactor() { return &_blendFactor; }
	uint32 GetSampleMask() { return _sampleMask; }
	ComPtr<ID3D11BlendState> GetComPtr() { return _blendState; }

    void Create(D3D11_RENDER_TARGET_BLEND_DESC blendDesc =
    {
        true,  // BlendEnable
        D3D11_BLEND_SRC_ALPHA,  // SrcBlend
        D3D11_BLEND_INV_SRC_ALPHA,  // DestBlend
        D3D11_BLEND_OP_ADD,  // BlendOp
        D3D11_BLEND_ONE,  // SrcBlendAlpha
        D3D11_BLEND_ZERO,  // DestBlendAlpha
        D3D11_BLEND_OP_ADD,  // BlendOpAlpha
        D3D11_COLOR_WRITE_ENABLE_ALL  // RenderTargetWriteMask
    }, float factor = 0.f);
private:
	ComPtr<ID3D11Device> _device;
	ComPtr<ID3D11BlendState> _blendState;
	float _blendFactor = 0.f;
	uint32 _sampleMask = 0xFFFFFFFF;
};

 

BlendState.cpp

#include "pch.h"
#include "BlendState.h"

BlendState::BlendState(ComPtr<ID3D11Device> device)
	:_device(device)
{
}

BlendState::~BlendState()
{
}

void BlendState::Create(D3D11_RENDER_TARGET_BLEND_DESC blendDesc, float factor)
{
	_blendFactor = factor;
	D3D11_BLEND_DESC desc;
	ZeroMemory(&desc, sizeof(desc));
	desc.AlphaToCoverageEnable = false;
	desc.IndependentBlendEnable = false;
	desc.RenderTarget[0] = blendDesc;

	HRESULT hr = _device->CreateBlendState(&desc, _blendState.GetAddressOf());
	CHECK(hr);
}

 

 

 

오늘부터는 공격시스템을 만들어 볼 것이다. 

일단 공격에 필요한 애니메이션 리소스는 Mixamo에서 가져다 쓰기로 하자 

 

공격용 애니메이션을 이전에 했던 방식과 같이 Retargeting해준다

오늘은 애니메이션 몽타주를 사용하여 여러 공격 애니메이션이 작동하도록 해주려고 한다. 

이때 애니메이션 몽타주(Animation Montage)는 정해진 순서에 따라 실행되는 애니메이션 사이에서 조건에 맞게 애니메이션 재생을 조절하는 기능으로 연속으로 재생해야 하는 여러 애니메이션 중 일부를 반복하거나 길게 재생하거나, 특정 애니메이션의 생략 혹은 조기 종료 등의 처리를 쉽게 해준다.

 

캐릭터를 기반으로 애니메이션 몽타주를 만들어준 뒤 애니메이션들을 원하는 순서대로 배치하고 

몽타주 섹션을 구분해주자

 

그리고 이제 마우스 왼쪽키를 누르면 이 애니메이션이 작동하도록 기존 애니메이션 그래프에 애니메이션 몽타주의 슬롯기능을 활용하여 그래프 흐름에 넣어주고 이를 코드를 통해 사용할 수 있도록 하자

 

일단 키 액션이 동작하도록 액션 변수를 선언해주고 바인딩 함수도 선언해준 뒤 애니메이션 몽타주 변수를 선언해주자 

 

SlashCharacter.h

UPROPERTY(EditAnywhere, Category = Input)
UInputAction* AttackAction;

void AttackPressed();

/**
	애니메이션 몽타주
*/
UPROPERTY(EditDefaultsOnly,Category=Montages)
UAnimMontage* AttackMontage;

 

SlashCharacter.cpp

void ASlashCharacter::AttackPressed()
{
	UAnimInstance* AnimInstance=GetMesh()->GetAnimInstance();
	if (AnimInstance && AttackMontage)
	{
		AnimInstance->Montage_Play(AttackMontage);
		int32 Selection = FMath::RandRange(0, 2);
		FName SectionName = FName();
		switch (Selection)
		{
		case 0:
			SectionName = FName("Attack1");
			break;
		case 1:
			SectionName = FName("Attack2");
			break;
		case 2:
			SectionName = FName("Attack3");
			break;
		default:
			break;
		}
		AnimInstance->Montage_JumpToSection(SectionName, AttackMontage);
	}
}

 

이렇게 한 뒤 에디터를 끄고 컴파일해주고 블루프린트상에 액션과 애니메이션 변수를 넣어주자 

 

이렇게 해주면 다른 공격 애니메이션이 작동하는 것을 볼 수 있다.

 

이때 공격버튼을 여러번 누르면 이상하게 작동하게 되는데 이를 막기위해 enum class를 통해 캐릭터의 state를 관리해주도록 하자.

Characterstate

enum class EActionState :uint8
{
	EAS_Unoccupied UMETA(DisplayName = "Unoccupied"),		
	EAS_Attacking UMETA(DisplayName = "Attacking")
};

SlashCharacter.h

EActionState ActionState = EActionState::EAS_Unoccupied;

 

SlashCharacter.cpp

void ASlashCharacter::AttackPressed()
{
	if (ActionState == EActionState::EAS_Unoccupied) 
	{
		PlayAttackMontage();
		ActionState = EActionState::EAS_Attacking;
	}
	
}

void ASlashCharacter::PlayAttackMontage()
{
	UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
	if (AnimInstance && AttackMontage)
	{
		AnimInstance->Montage_Play(AttackMontage);
		const int32 Selection = FMath::RandRange(0, 2);
		FName SectionName = FName();
		switch (Selection)
		{
		case 0:
			SectionName = FName("Attack1");
			break;
		case 1:
			SectionName = FName("Attack2");
			break;
		case 2:
			SectionName = FName("Attack3");
			break;
		default:
			break;
		}
		AnimInstance->Montage_JumpToSection(SectionName, AttackMontage);
		ActionState = EActionState::EAS_Unoccupied;
	}
}

 

그리고 하나의 공격액션이 끝나면 다른 공격액션도 수행할 수 있도록 해주자 

anim notify를 만들어주고 이 이벤트를 통해 state를 초기화해주자

 

SlashCharacter.cpp

void ASlashCharacter::AttackEnd()
{
	ActionState = EActionState::EAS_Unoccupied;
}

 

그리고 무기를 장착하고 있을 때만 이 애니메이션이 나오도록 하자

SlashCharacter.cpp

void ASlashCharacter::AttackPressed()
{
	const bool bCanAttack = 
		ActionState == EActionState::EAS_Unoccupied && 
		CharacterState != ECharacterState::ECS_Unequipped;
	if (bCanAttack) 
	{
		PlayAttackMontage();
		ActionState = EActionState::EAS_Attacking;
	}
	
}

 

이렇게 하면 무기를 장착하고 있을 때만 공격모션이 나오게 된다.

공격모션이 실행될 때 소리가 같이 들리도록 해보자

 

알맞은 사운드를 다운받고 임폴트 해준 뒤에 메타사운드를 통해 사운드를 설정하고 

사운드 노티파이를 통해 재생되게 하면 된다.

 

이와 같은 방법으로 걸을 때와 점프할 때에도 메타사운드를 만들고 이를 추가해주자

 

그리고 지금 서 있을 때의 애니메이션에서 두 발이 너무 붙어있기 때문에 이것을 조금 고쳐주도록하자 이게 너무 붙어있는 이유는 IK_foot 애니메이션에서 IK_foot의 위치에 맞게 발의 위치를 조정하고 있기 때문에 원래 발의 위치를 가져와서

IK_foot의 위치를 조정해주어야한다. 

이 애니메이션이 아래와 같이 블루프린트를 설정해주게 되면 

이렇게 바뀌게 된다.

 

이제 무기를 등 뒤에서 장착하고 장착해제하는 애니메이션을 추가해보자 애니메이션은 Mixamo의 axe에서 가져오자

전에 했던 것과 같이 import후 Retargeting 해준다. 그리고 이를 바탕으로 애니메이션 몽타주를 만들어주자 

이때 Equip와 Unequip의 몽타주 섹션을 지정해줘서 코드로 애니메이션 재생을 컨트롤할 수 있게하자

 

이제 코드를 통해 각각의 애니메이션이 실행되도록하자 이때 무기를 들면 그 무기를 변수에 저장해서 장착해제도 가능하도록 하자.

SlashCharacter.h

	void PlayEquipMontage(FName SectionName);
	bool CanDisarm();
	bool CanArm();
    
	UPROPERTY(VisibleAnywhere, Category = Weapon)
	AWeapon* EquippedWeapon;
    
    UPROPERTY(EditDefaultsOnly, Category = Montages)
	UAnimMontage* EquipMontages;

 

SlashCharacter.cpp

void ASlashCharacter::EKeyPressed()
{
	AWeapon* OverlappingWeapon = Cast<AWeapon>(OverlappingItem);
	if (OverlappingWeapon)
	{
		OverlappingWeapon->Equip(GetMesh(), FName("RightHandSocket"));
		CharacterState = ECharacterState::ECS_EquippedOneHandedWeapon;
		OverlappingItem = nullptr;
		EquippedWeapon = OverlappingWeapon;
	}
	else
	{
		if (CanDisarm())
		{
			PlayEquipMontage(FName("Unequip"));
			CharacterState = ECharacterState::ECS_Unequipped;
		}
		else if (CanArm())
		{
			PlayEquipMontage(FName("Equip"));
			CharacterState = ECharacterState::ECS_EquippedOneHandedWeapon;
		}
	}
}

void ASlashCharacter::PlayEquipMontage(FName SectionName)
{
	UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
	if (AnimInstance && EquipMontages)
	{
		AnimInstance->Montage_Play(EquipMontages);
		AnimInstance->Montage_JumpToSection(SectionName, EquipMontages);
	}
}


bool ASlashCharacter::CanDisarm()
{
	return ActionState == EActionState::EAS_Unoccupied && 
		CharacterState != ECharacterState::ECS_Unequipped;
}

bool ASlashCharacter::CanArm()
{
	return ActionState == EActionState::EAS_Unoccupied &&
		CharacterState == ECharacterState::ECS_Unequipped &&
		EquippedWeapon;
}

 

이렇게하면 각각의 애니메이션이 e키를 누를때 실행된다.

 

이제 장착해제하고 장착할 때 등에 넣고 등에서 꺼내올 수 있게 하자 이는 소캣을 활용하여 만들 수 있다.

 

 

SlashCharacter.h

	UFUNCTION(BlueprintCallable)
	void Disarm();
    
    
	UFUNCTION(BlueprintCallable)
	void Arm();


SlashCharacter.cpp

void ASlashCharacter::Disarm()
{
	if (EquippedWeapon)
	{
		EquippedWeapon->AttachMeshToSocket(GetMesh(), FName("SpineSocket"));
	}
}

void ASlashCharacter::Arm()
{
	if (EquippedWeapon)
	{
		EquippedWeapon->AttachMeshToSocket(GetMesh(), FName("RightHandSocket"));
	}
}

 

Weapon.cpp

void AWeapon::AttachMeshToSocket(USceneComponent* Inparent, const FName& InSocketName)
{
	FAttachmentTransformRules TransformRules(EAttachmentRule::SnapToTarget, true);
	ItemMesh->AttachToComponent(Inparent, TransformRules, InSocketName);
}

 

이렇게하고 장착해제 애니메이션 몽타주에 Anim Notify를 추가해준 뒤 이 Notify에서 Disarm 함수를 부르도록 하자

 

이렇게 하면 소캣에 넣고 빼는 것이 가능해진다.

 

그리고 달릴 때 E키를 누르면 달리기를 멈추고 동작을 하도록 바꾸자 

일단 애니메이션 몽타주에 notify를 추가해주고 콜백 함수를 만들어준 뒤 블루프린트에서 이 함수를 호출하도록하자

 

SlashCharacter.h

	UFUNCTION(BlueprintCallable)
	void FinishEquipping();

 

SlashCharacter.cpp

if (CanDisarm())
{
	PlayEquipMontage(FName("Unequip"));
	CharacterState = ECharacterState::ECS_Unequipped;
	ActionState = EActionState::EAS_EquippingWeapon;
}
else if (CanArm())
{
	PlayEquipMontage(FName("Equip"));
	CharacterState = ECharacterState::ECS_EquippedOneHandedWeapon;
	ActionState = EActionState::EAS_EquippingWeapon;
}

void ASlashCharacter::FinishEquipping()
{
	ActionState = EActionState::EAS_Unoccupied;
}

 

 

 

이렇게 해주면 달리는 중에  E키를 누르면 멈추고 행동을 한다.

오늘부터는 만들어둔 코드를 하나의 프레임워크로 만들어 볼 것이다.

 

1. Graphics

일단 만든코드를 기능에 따라 클래스로 나눠보자

Graphics 클래스에 선언해주자

이 클래스는 

1. 디바이스 및 디바이스 컨텍스트 생성

  • 단계: Direct3D 디바이스와 디바이스 컨텍스트를 생성
  • 역할: GPU와 통신하고, 그리기 명령을 실행하며, 리소스를 생성하고 관리

2. 스왑 체인 생성

  • 단계: 스왑 체인을 생성
  • 역할: 렌더링된 이미지를 화면에 표시하는 버퍼를 관리

3. 렌더 타겟 뷰 생성

  • 단계: 렌더 타겟 뷰를 생성
  • 역할: 렌더링 결과를 출력할 대상(렌더 타겟)을 설정

4. 뷰포트 설정

  • 단계: 뷰포트를 설정
  • 역할: 화면에 그려질 영역을 설정

Graphics.h

#pragma once
class Graphics
{
public:
	Graphics(HWND hwnd);
	~Graphics();

	void RenderBegin();
	void RenderEnd();

	ComPtr<ID3D11Device> GetDevice() { return _device; }
	ComPtr<ID3D11DeviceContext> GetDeviceContext() { return _deviceContext; }

private:
	void CreateDeviceAndSwapChain();
	/// <summary>
	/// 버퍼를 묘사 Tag를 달아서 GPU에 알려주기 위함
	/// </summary>
	void CreateRenderTargetView();
	/// <summary>
	/// 뷰포트 묘사 
	/// </summary>
	void SetViewport();
private:
	HWND _hwnd;
	uint32 _width = 0;
	uint32 _height = 0;

private:
	//Device & SwapChain
	// I: 인터페이스 Comptr -> 스마트 포인터- 자동관리해줌, wrl에 있다.
	ComPtr<ID3D11Device> _device;
	ComPtr<ID3D11DeviceContext> _deviceContext;
	//DXGI : DX와는 독립적으로 하위 수준 작업 관리 => 시스템,하드웨어 통신
	//스왑체인-> 그리는 것과 보여주는 것 따로 해야 보통 방식 ->전면 후면 고속복사
	ComPtr<IDXGISwapChain> _swapChain = nullptr;

	///RTV 렌더타켓뷰
	ComPtr<ID3D11RenderTargetView> _renderTargetView;

	//Misc 
	D3D11_VIEWPORT _viewport = { 0 };
	float _clearColor[4] = { 0.f,0.f,0.f,0.f };
};

Graphics.cpp

#include "pch.h"
#include "Graphics.h"

Graphics::Graphics(HWND hwnd)
{
	_hwnd = hwnd;

	// 초기화 순서 확인
	_width = GWinSizeX;
	_height = GwinSizeY;

	CreateDeviceAndSwapChain();
	CreateRenderTargetView();
	SetViewport();
}

Graphics::~Graphics()
{
}

void Graphics::RenderBegin()
{
	//여기다가 그릴것이다.
	_deviceContext->OMSetRenderTargets(1, _renderTargetView.GetAddressOf(), nullptr);				//OM: OUTPUT Mege
	_deviceContext->ClearRenderTargetView(_renderTargetView.Get(), _clearColor);		//색상으로 초기화해주기
	_deviceContext->RSSetViewports(1, &_viewport);
}

void Graphics::RenderEnd()
{
	HRESULT hr = _swapChain->Present(1, 0);			//제출 전면 <= 후면 
	CHECK(hr);
}

void Graphics::CreateDeviceAndSwapChain()
{
	DXGI_SWAP_CHAIN_DESC desc;
	ZeroMemory(&desc, sizeof(desc));		//다 0으로 초기화해줌 필요한 것만 따로 초기화
	{
		desc.BufferDesc.Width = _width;      // 버퍼도 같은 크기로 800 x 600
		desc.BufferDesc.Height = _height;
		desc.BufferDesc.RefreshRate.Numerator = 60;			//화면 주사율
		desc.BufferDesc.RefreshRate.Denominator = 1;
		desc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
		desc.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;
		desc.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;
		desc.SampleDesc.Count = 1;			//멀티 샘플링 계단현상, 찌그러짐 어떻게 처리할지 
		desc.SampleDesc.Quality = 0;
		desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;			//버퍼를 어떻게 쓸건지 - 그려주는 역활
		desc.BufferCount = 1;		//버퍼몇개
		desc.OutputWindow = _hwnd;		// 결과 윈도우핸들
		desc.Windowed = true;
		desc.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
	}
	//HRESULT 일종의 bool값
	HRESULT hr = ::D3D11CreateDeviceAndSwapChain(
		nullptr,
		D3D_DRIVER_TYPE_HARDWARE,			//어떤걸 쓸건지 -> gpu or software로 gpu
		nullptr,
		0,
		nullptr,				//지원하는 dx 버전 레벨 기본값은 내가 지원할 수 있는 상위버전 골라줌 
		0,						//위에 배열 크기
		D3D11_SDK_VERSION,				//매크로
		&desc,
		_swapChain.GetAddressOf(),			//**이면 주소값 가져오는 GetAddressOf()
		_device.GetAddressOf(),
		nullptr,
		_deviceContext.GetAddressOf()
	);

	CHECK(hr);
}

void Graphics::CreateRenderTargetView()
{
	HRESULT hr;

	ComPtr<ID3D11Texture2D> backBuffer = nullptr;
	hr = _swapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), (void**)backBuffer.GetAddressOf());		//백버퍼 텍스처(PNG)로 가져오기
	CHECK(hr);

	//택스처라는건 그냥 쓰면 아무것도 없는데 거기에 최종 그림을 그리는 곳에 쓸거라는 태그를 달아주는 것 
	_device->CreateRenderTargetView(backBuffer.Get(), nullptr, _renderTargetView.GetAddressOf());  //렌더타켓뷰라는것으로 텍스처를 명시해 만들어준다.
	CHECK(hr);
}

void Graphics::SetViewport()
{
	_viewport.TopLeftX = 0.f;
	_viewport.TopLeftY = 0.f;
	_viewport.Width = static_cast<float>(_width);
	_viewport.Height = static_cast<float>(_height);
	_viewport.MinDepth = 0.f;
	_viewport.MaxDepth = 1.f;
}

 

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

 

2.Input Assembler

InputAssembler에 해당하는 부분을 각각의 클래스로 만들어주자 

일단 선언한 정점의 정보를 받아서 gpu쪽으로 넘겨주는

VertexBuffer에 해당하는 부분을 추출해서 클래스로 만들어주자

VertexBuffer.h

#pragma once

//지오메트리로 만들어 준 것을 넘겨줘서 사용
class VertexBuffer
{
public:
	VertexBuffer(ComPtr<ID3D11Device> _device);
	~VertexBuffer();

	ComPtr<ID3D11Buffer> GetComPtr() { return _vertexBuffer; }
	uint32 GetStride() { return _stride; }
	uint32 GetOffset() { return _offset; }
	uint32 GetCount() { return _count; }

	template<typename T>
	void Create(const vector<T>& vertices)
	{
		_stride = sizeof(T);		//크기
		_count = static_cast<uint32>(vertices.size());		//정점의 개수


		D3D11_BUFFER_DESC desc;
		ZeroMemory(&desc, sizeof(desc));
		desc.Usage = D3D11_USAGE_IMMUTABLE;			//gpu만 read only
		desc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
		desc.ByteWidth = (uint32)(_stride*_count);

		D3D11_SUBRESOURCE_DATA data; 
		ZeroMemory(&data, sizeof(data));
		data.pSysMem = vertices.data();			//첫번째 시작주소 cpu값이 복사된다.

		HRESULT hr = _device->CreateBuffer(&desc, &data, _vertexBuffer.GetAddressOf());
		CHECK(hr);
	}

private:
	ComPtr<ID3D11Device> _device;
	ComPtr<ID3D11Buffer> _vertexBuffer;

	uint32 _stride = 0;
	uint32 _offset = 0;
	uint32 _count = 0;
};

 

VertexBuffer.cpp

#include "pch.h"
#include "VertexBuffer.h"

VertexBuffer::VertexBuffer(ComPtr<ID3D11Device> device)
	:_device(device)
{

}

VertexBuffer::~VertexBuffer()
{
}

 

원래 클래스에서 VertexBuffer에 해당하는 부분도 바꿔주도록하자

Game.h

private:
	//기하학적 도형 - cpu
	vector<Vertex> _vertices;
	VertexBuffer* _vertexBuffer;

 

Game.cpp

void Game::Init(HWND hwnd)
{
	_hwnd = hwnd;

	//_graphics=make_shared<Graphics>(hwnd):
	_graphics = new Graphics(hwnd);
	_vertexBuffer = new VertexBuffer(_graphics->GetDevice());

	//삼각형 그리기 파트
	CreateGeometry();
	CreateVS();
	CreateInputLayout();
	CreatePS();


	CreateRasterizerState();
	CreateSamplerState();
	CreateBlenderState();

	CreateSRV();
	CreateConstantBuffer();
}

void Game::Render()
{
	_deviceContext->IASetVertexBuffers(0, 1, _vertexBuffer->GetComPtr().GetAddressOf(), &stride, &offset);
}

void Game::CreateGeometry()
{
	
	//정점버퍼
	{
		_vertexBuffer->Create(_vertices);
	}
}

 

두번째로

정점의 인덱스정보를 담은 버퍼를 생성하는 

IndexBuffer부분을 추출해서 클래스로 만들어주자 

IndexBuffer.h

#pragma once
class IndexBuffer
{
public:
	IndexBuffer(ComPtr<ID3D11Device> device);
	~IndexBuffer();

	ComPtr<ID3D11Buffer> GetComPtr() { return _indexBuffer; }
	uint32 GetStride() { return _stride; }
	uint32 GetOffset() { return _offset; }
	uint32 GetCount() { return _count; }

	void Create(const vector<uint32>& indices);

private:
	ComPtr<ID3D11Device> _device;
	ComPtr<ID3D11Buffer> _indexBuffer;

	uint32 _stride = 0;
	uint32 _offset = 0;
	uint32 _count = 0;
};

 

IndexBuffer.cpp

#include "pch.h"
#include "IndexBuffer.h"

IndexBuffer::IndexBuffer(ComPtr<ID3D11Device> device)
	:_device(device)
{
}

IndexBuffer::~IndexBuffer()
{
}

void IndexBuffer::Create(const vector<uint32>& indices)
{
	_stride = sizeof(uint32);
	_count = static_cast<uint32>(indices.size()); 

	D3D11_BUFFER_DESC desc;
	ZeroMemory(&desc, sizeof(desc));
	desc.Usage = D3D11_USAGE_IMMUTABLE;			//gpu만 read only
	desc.BindFlags = D3D11_BIND_INDEX_BUFFER;
	desc.ByteWidth = (uint32)(_stride * _count);

	D3D11_SUBRESOURCE_DATA data;
	ZeroMemory(&data, sizeof(data));
	data.pSysMem = indices.data();			//첫번째 시작주소 cpu값이 복사된다.

	HRESULT hr = _device->CreateBuffer(&desc, &data, _indexBuffer.GetAddressOf());
	CHECK(hr);
}

 

클래스로 만든 부분을 수정해주자

Game.h

	IndexBuffer* _indexBuffer;

 

Game.cpp

void Game::Init(HWND hwnd)
{
	_indexBuffer = new IndexBuffer(_graphics->GetDevice());
}

void Game::Render()
{
	_deviceContext->IASetIndexBuffer(_indexBuffer->GetComPtr().Get(), DXGI_FORMAT_R32_UINT, 0);
}

void Game::CreateGeometry()
{
        //IndexBuffer
    {
        _indexBuffer->Create(_indices);
    }
}

동일한 결과가 나온다.

 

마지막으로 

쉐이더가 만들어진 상태에서 정점버퍼에 있는 내용을 쉐이더에서 어떻게 해석하는지에 대한 정보를 담은 부분인

IndexLayout부분을 추출해서 클래스로 만들어주자 

이때 정점정보와 레이아웃, 쉐이더상의 정점의 구조가 매핑이 알맞게 되어야한다.

InputLayout.h

#pragma once
class InputLayout
{
public:
	InputLayout(ComPtr<ID3D11Device> device);
	~InputLayout();

	ComPtr<ID3D11InputLayout> GetComPtr() { return _inputLayout; }

	void Create(const vector<D3D11_INPUT_ELEMENT_DESC>& descs,ComPtr<ID3DBlob> blob);

private:
	ComPtr<ID3D11Device> _device;
	ComPtr<ID3D11InputLayout> _inputLayout;
};

 

InputLayout.cpp

#include "pch.h"
#include "InputLayout.h"

InputLayout::InputLayout(ComPtr<ID3D11Device> device)
	:_device(device)
{
}

InputLayout::~InputLayout()
{
}

void InputLayout::Create(const vector<D3D11_INPUT_ELEMENT_DESC>& descs, ComPtr<ID3DBlob> blob)
{ 
	//몇개의 원소를 가지고 있는지
	const int32 count = static_cast<int32>(descs.size());
	//VS과정의 입력에 관련있어서 vsBlob을 매개변수로 해줌
	_device->CreateInputLayout(descs.data(), count, blob->GetBufferPointer(), blob->GetBufferSize(), _inputLayout.GetAddressOf());

}

 

클래스로 만든 부분을 수정해주자

Game.h

	InputLayout* _inputLayout;

Game.cpp

void Game::Init(HWND hwnd)
{
	_inputLayout = new InputLayout(_graphics->GetDevice());
}

void Game::Render()
{
	_deviceContext->IASetInputLayout(_inputLayout->GetComPtr().Get());
}

void Game::CreateInputLayout()
{
	//입력에 대한 정보 ~바이트부터 뛰면 칼러인지 
	vector<D3D11_INPUT_ELEMENT_DESC> layout
	{
		{"POSITION",0,DXGI_FORMAT_R32G32B32_FLOAT,0,0,D3D11_INPUT_PER_VERTEX_DATA,0},
		{"TEXCOORD",0,DXGI_FORMAT_R32G32_FLOAT,0,12,D3D11_INPUT_PER_VERTEX_DATA,0}
	};
	_inputLayout->Create(layout,_vsBlob);
	
}

 

동일한 결과가 나온다.

 

3.Geometry

하드코딩된 정점이 아닌 기하학적인 정보와 모형을 받아주는 Geometry 부분을 만들어보자

처음으로 기하학적인 정보를 받을 Geometry 클래스와

기하학적인 도형을 만들어주는 GeometryHelper클래스,

 

처음으로 기하학적인 정보 vertices와 Indices를 가지고 있을 Geometry 클래스를 만들자

이때 제네릭 벡터로 정점을 정의해서 어떤 타입이든 받을 수 있게하고 정점의 개수가 동적으로 바뀔 수 있게 함수를 선언해준다.

Geometry.h

#pragma once

template<typename T>
class Geometry
{
public:
	Geometry();
	~Geometry();

	//정점개수
	uint32 GetVertexCount() { return static_cast<uint32>(_vertices.size()); }

	void* GetVertexData() { return _vertices.data(); }
	const vector<T>& GetVertices() { return _vertices; }

	uint32 GetIndexCount() { return static_cast<uint32>(_indices.size()); }
	void* GetIndexData() { return _indices.data(); }
	const vector<uint32>& GetIndices() { return _indices; }

	void AddVertex(const T& vertex) { _vertices.push_back(vertex); }
	void AddVertices(const vector<T>& vertices) { _vertices.insert(_vertices.end(), vertices.begin(), vertices.end()); }
	void SetVertices(const vector<T>& vertices) { _vertices = vertices; }

	void AddIndex(uint32 index) { _indices.push_back(index); }
	void AddIndices(const vector<uint32>& indices) { _indices.insert(_indices.end(), indices.begin(), indices.end()); }
	void SetIndices(const vector<uint32>& indices) { _indices = indices; }
private:
	vector<T> _vertices;
	vector<uint32> _indices;
};

Geometry.cpp

 

그리고 정점의 struct 정보를 가지고 있을 VertexData를 만들어주자

앞으로 정점에 대한 struct정보가 필요할 경우 추가해줄 곳이 될 것이다.

각 Stuct에서 desc를 가지고 있어서 각 정정 입력에 대한 InputLayout, 쉐이더에서 필요한 정보를 줄 수 있도록 하자.

VertexData.h

#pragma once

struct VertexTextureData
{
	Vec3 position = { 0,0,0 };		//12바이트 0부터시작
	Vec2 uv = { 0,0 };

	static vector<D3D11_INPUT_ELEMENT_DESC> descs;
};

struct VertexColorData
{
	Vec3 position = { 0,0,0 };		//12바이트 0부터시작
	Color color = { 0,0,0,0 };		//12부터시작

	static vector<D3D11_INPUT_ELEMENT_DESC> descs;
};

 

VertexData.cpp

#include "pch.h"
#include "VertexData.h"

static vector<D3D11_INPUT_ELEMENT_DESC> VertexTextureData::descs =
{
	{"POSITION",0,DXGI_FORMAT_R32G32B32_FLOAT,0,0,D3D11_INPUT_PER_VERTEX_DATA,0},
	{"TEXCOORD",0,DXGI_FORMAT_R32G32_FLOAT,0,D3D11_APPEND_ALIGNED_ELEMENT,D3D11_INPUT_PER_VERTEX_DATA,0},
};

static vector<D3D11_INPUT_ELEMENT_DESC> VertexColorData::descs =
{
	{"POSITION",0,DXGI_FORMAT_R32G32B32_FLOAT,0,0,D3D11_INPUT_PER_VERTEX_DATA,0},
	{"COLOR",0,DXGI_FORMAT_R32G32B32A32_FLOAT,0,D3D11_APPEND_ALIGNED_ELEMENT,D3D11_INPUT_PER_VERTEX_DATA,0},
};

 

그리고 마지막으로 사각형과 같은 기본적인 도형을 만드는 함수들이 포함된 GeometryHelper 클래스를 만들어주자

GeometryHelper.h

#pragma once
class GeometryHelper
{
public:
	static void CreateRectangle(shared_ptr<Geometry<VertexColorData>> geometry,Color color);
	static void CreateRectangle(shared_ptr<Geometry<VertexTextureData>> geometry);
};

 

GeometryHelper.cpp

#include "pch.h"
#include "GeometryHelper.h"

void GeometryHelper::CreateRectangle(shared_ptr<Geometry<VertexColorData>> geometry, Color color)
{
	vector<VertexColorData> vertices;
	_vertices.resize(4);

	//13 -> 012
	//02 -> 213
	vertices[0].position = Vec3(-0.5f, -0.5f, 0.f);
	vertices[0].color = color;
	vertices[1].position = Vec3(-0.5f, 0.5f, 0.f);
	vertices[1].color = color;
	vertices[2].position = Vec3(0.5f, -0.5f, 0.f);
	vertices[2].color = color;
	vertices[3].position = Vec3(0.5f, 0.5f, 0.f);
	vertices[3].color = color;
	geometry->SetVertices(vertices);

	vector<uint32> indices = { 0,1,2,2,1,3 };
	geometry->SetIndices(indices);
}

void GeometryHelper::CreateRectangle(shared_ptr<Geometry<VertexTextureData>> geometry)
{
	vector<VertexTextureData> vertices;
	_vertices.resize(4);

	//13 -> 012
	//02 -> 213
	vertices[0].position = Vec3(-0.5f, -0.5f, 0.f);
	vertices[0].uv = Vec2(0.f, 1.f);
	vertices[1].position = Vec3(-0.5f, 0.5f, 0.f);
	vertices[1].uv = Vec2(0.f, 0.f);
	vertices[2].position = Vec3(0.5f, -0.5f, 0.f);
	vertices[2].uv = Vec2(1.f, 1.f);
	vertices[3].position = Vec3(0.5f, 0.5f, 0.f);
	vertices[3].uv = Vec2(1.f, 0.f);
	geometry->SetVertices(vertices);

	vector<uint32> indices = { 0,1,2,2,1,3 };
	geometry->SetIndices(indices);
}

 

이제 우리의 메인코드인 Game 클래스를 고쳐주자

Game.h

	
private:
	shared_ptr<Graphics> _graphics;
	//기하학적 도형 - cpu
	//vector<Vertex> _vertices;
	//vector<uint32> _indices;

	shared_ptr<Geometry<VertexTextureData>> _geometry;

	shared_ptr<VertexBuffer> _vertexBuffer;
	
	//인덱스버퍼 - 이거도 Geometry에 포함
	shared_ptr<IndexBuffer> _indexBuffer;
	shared_ptr<InputLayout> _inputLayout;

Game.cpp

void Game::Init(HWND hwnd)
{

    _graphics = make_shared<Graphics>(hwnd);
    _vertexBuffer = make_shared<VertexBuffer>(_graphics->GetDevice());
    _indexBuffer = make_shared<IndexBuffer>(_graphics->GetDevice());
    _inputLayout = make_shared<InputLayout>(_graphics->GetDevice());
    _geometry = make_shared<Geometry<VertexTextureData>>();
}

void Game::Render()
{
	uint32 stride = sizeof(VertexTextureData);
}


void Game::CreateGeometry()
{
	//정점정보
	GeometryHelper::CreateRectangle(_geometry);

	//정점버퍼
	_vertexBuffer->Create(_geometry->GetVertices());

	//IndexBuffer
	_indexBuffer->Create(_geometry->GetIndices());
}

 

이렇게하면 동일한 결과가 나오게 된다.

 

 

 

오늘부터는 무기를 만들어볼 것이다 

일단 아이템클래스를 상속받는 C++ 클래스를 만들어주고 이를 바탕으로하는 블루프린트 클래스를 만들어주자 

 

이 Weapon 클래스는 아이템클래스를 상속받기 때문에 아이템이 가지고있는 기능을 쓸 수 있다.

 

아이템 클래스에서 오버랩에 빠져나갈 때와 들어갔을 때의 이벤트를 virtual로 만들어서 Weapon 클래스에서 override

할 수 있게 만들어주자

Item.h

UFUNCTION()
virtual void OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);

UFUNCTION()
virtual void OnSphereEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);

 

Weapon.h

	virtual void OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult) override;

	virtual void OnSphereEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)override;

Weapon.cpp

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


#include "Items/Weapons/Weapon.h"

void AWeapon::OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{

}

void AWeapon::OnSphereEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{

}

 

이렇게 하면 override된 함수에서는 아무런 기능을 구현해두지 않았기 때문에 아무런 효과가 나타나지 않는다.

 

만약 여기서 Super를 통해 부모의 함수를 가져와 사용하게 된다면 다시 글이 나타나게 된다.

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


#include "Items/Weapons/Weapon.h"

void AWeapon::OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	Super::OnSphereOverlap(OverlappedComponent, OtherActor, OtherComp, OtherBodyIndex, bFromSweep, SweepResult);
}

void AWeapon::OnSphereEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
	Super::OnActorEndOverlap(OverlappedComponent, OtherActor, OtherComp, OtherBodyIndex);
}

 

이제 애니메이션을 추가해보자 

 

캐릭터 애니메이션은 보통 Mixamo에서 무료 애니메이션을 찾아서 다운받을 수 있다

https://www.mixamo.com/#/

 

Mixamo

 

www.mixamo.com

 

나는 여기중에서  xbot을 다운해서 사용해 볼 것이다.

 

다운받을 때 설정은 기본값으로 유지한채로 다운받아주자

 

그리고 받은 에셋을 임폴트 해주자 마찬가지로 설정값은 디폴트상태로 유지한채로 임폴트해주

 

이제 애니메이션을 다운받아주자 애니메이션은 axe 애니메이션중에 Standing Idle와 Standing Melee Attack  을 다운받아주자 다운로드  설정값은 Skin부분만 Without Skin으로 맞춰주고 다운로드해주자 

 

이제 이 애니메이션을 임폴트해주면 되는데 이때 매시를 다운받은 Xbot 매시로 지정해주면 된다

 

이제 이 애니메이션을 기존에 만들어둔 캐릭터에서도 사용할 수 있도록 IK리타겟팅을 해주어야한다. 이를 위해 SKM_Xbot을 기반으로 IK Rigs를 만들어주고 이를 통해 리타겟팅 체인을 만들어주자

 

 

이제 이것을 캐릭터에도 동일하게 해주면 된다.

 

이제 애니메이션/IK 리타켓터를 만들어둔  Xbot의 IK Rig를 바탕으로 만들어서 캐릭터로 리타겟팅해보자 

리타켓팅할때는 기본 포즈가 Xbot과 같도록 팔의 각도를 바꿔주면서 자세 애니메이션이 자연스러운지 확인해보면서 

만들면된다.

 

그리고 나서는 Export하고자하는 애니메이션을 클릭한뒤 Export 해주면된다.

 

이렇게하게되면 애니메이션을 추출해서 사용할 수 있다.

 

이제 칼을 실제로 들고 있을 수 있도록 해보자 

일단 블루프린트로 먼저 구현하고 C++로 구현해보자

블루프린트에서는 Collision이 시작될 때 캐릭터의 RightHandSocket에 Attach되도록 만들어주자

이렇게 하게되면 충돌시 무기가 Socket자리에 들어가게 된다.

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

 FAttachmentTransformRules을 선언해주고 AttachToComponent로 붙여주면 된다.

void AWeapon::OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	Super::OnSphereOverlap(OverlappedComponent, OtherActor, OtherComp, OtherBodyIndex, bFromSweep, SweepResult);

	ASlashCharacter* SlashCharacter = Cast<ASlashCharacter>(OtherActor);
	if (SlashCharacter)
	{
		FAttachmentTransformRules TransformRules(EAttachmentRule::SnapToTarget,true);
		ItemMesh->AttachToComponent(SlashCharacter->GetMesh(), TransformRules, FName("RightHandSocket"));
	}
}

 

결과

 

 

이제 E키를 통해 무기를 주울 수 있게 구현해보자 

일단 만약 아이템근처에 가게되면 캐릭터에서 그 아이템을 식별할 수 있도록 AItem 포인터 변수를 선언해주고 Set함수를 통해 아이템근처에 가면 그 변수를 초기화해주고 근처에서 벗어나면 nullptr로 바뀌도록 해주자

 

SlashCharacter.h

private:
	UPROPERTY(VisibleInstanceOnly)
	AItem* OverlappingItem;
public:
	FORCEINLINE void SetOverlappingItem(AItem* Item) { OverlappingItem = Item; }

 

Item.cpp

void AItem::OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	ASlashCharacter* SlashCharacter = Cast<ASlashCharacter>(OtherActor);
	if (SlashCharacter)
	{
		SlashCharacter->SetOverlappingItem(this);
	}
}

void AItem::OnSphereEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
	ASlashCharacter* SlashCharacter = Cast<ASlashCharacter>(OtherActor);
	if (SlashCharacter)
	{
		SlashCharacter->SetOverlappingItem(nullptr);
	}
}

 

그리고 만약 E키를 누르면 무기가 소켓안으로  Attach될수있도록 E키 액션바인딩함수와 무기클래스에서 Attach함수를 만들어 작동할 수 있도록 구현하자

 

Weapon.h

public:
	void Equip(USceneComponent* Inparent,FName InSocketName);

Weapon.cpp

void AWeapon::Equip(USceneComponent* Inparent, FName InSocketName)
{
	FAttachmentTransformRules TransformRules(EAttachmentRule::SnapToTarget, true);
	ItemMesh->AttachToComponent(Inparent, TransformRules, InSocketName);
}

 

SlashCharacter.h

protected:
UPROPERTY(EditAnywhere, Category = Input)
UInputAction* EKeyAction;
    
void EKeyPressed();

 

SlashCharacter.cpp

void ASlashCharacter::EKeyPressed()
{
	AWeapon* OverlappingWeapon = Cast<AWeapon>(OverlappingItem);
	if (OverlappingItem)
	{
		OverlappingWeapon->Equip(GetMesh(), FName("RightHandSocket"))
	}
}

void ASlashCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

	if (UEnhancedInputComponent* EnhancedInputComponent = CastChecked<UEnhancedInputComponent>(PlayerInputComponent))
	{
		EnhancedInputComponent->BindAction(MovementAction, ETriggerEvent::Triggered, this, &ASlashCharacter::Move);
		EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &ASlashCharacter::Look);
		EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Triggered, this, &ACharacter::Jump);
		EnhancedInputComponent->BindAction(EKeyAction, ETriggerEvent::Triggered, this, &ASlashCharacter::EKeyPressed);
	}
}

 

이렇게하고 

키액션을 바인딩해주면 E키를 누르면 무기가 주워지는 모습을 볼 수 있다.

 

결과화면

 

무기를 가지고있을 때의 애니메이션을 적용시켜주기위해 Enum Class CharacterState를 만들어주고

무기를 착용하면 캐릭터의 State가 바뀌도록 만들고 이를 블루프린트에서 볼 수 있도록 적용해주자

CharacterTypes.h

#pragma once


UENUM(BlueprintType)
enum class ECharacterState : uint8
{
	ECS_Unequipped UMETA(DisplayName = "Unequipped"),
	ECS_EquippedOneHandedWeapon UMETA(DisplayName = "Equipped One-Handed Weapon"),
	ECS_EquippedTwoHandedWeapon UMETA(DisplayName = "Equipped Two-Handed Weapon")
};

SlashCharacter.h

	FORCEINLINE ECharacterState GetCharacterState() const { return CharacterState; }

 

SlashAnimInstance.h

	UPROPERTY(BlueprintReadOnly,Category="Movement | Character State")
	ECharacterState CharacterState;

SlashAnimInstance.cpp

void ASlashCharacter::EKeyPressed()
{
	AWeapon* OverlappingWeapon = Cast<AWeapon>(OverlappingItem);
	if (OverlappingItem)
	{
		OverlappingWeapon->Equip(GetMesh(), FName("RightHandSocket"));
		CharacterState = ECharacterState::ECS_EquippedOneHandedWeapon;
	}
}

 

그리고 Blend pose by EcharacterState를 사용하여 각각의 State에 따라 다른 애니메이션이 출력될 수 있도록 하자

 

이렇게 해주면 이제 무기를 쥐고있을 때 다른 애니메이션이 출력된다.

 

이제 무기를 쥐고 달리는 애니메이션을 추가해보자 

Mixamo에서 axe의 Standing Run Forward 애니메이션에서 In Place가 체크된 애니메이션을 다운받아서 임폴트 해주자

전에 했던것과 같이 리타켓팅을 통해 애니메이션을 Export 해주자

 

그리고 위에 했던 것과 같이 Blend를 통해 애니메이션이 출력되도록 해주자

 

그런데 이렇게 하게되면 애니메이션이 추가될때마다 블루프린트가 복잡해지게 된다.

다양한 애니메이션을 Mutiple Animation BluePrint를 통해 관리해줄 수 있다.

 

일단 캐릭터의 주요한 애니메이션을 담을 ABP_Echo_MainStates 애니메이션 블루프린트를 새로 만들어주자 

그리고  Main state와 Ground Locomotion을 복사 붙여넣기 해주자

이상태로 컴파일해주게 되면 오류가 발생하는데 변수까지 복사를 해주어서 발생하는 문제점이다. 일단 오류가 발생하는 변수들을 모두 생성해주자 

이 변수들은 블루프린트에서 만들어진 변수로 캐릭터의 상태가 바뀌어도 변수의 값이 변하지 않기때문에 주의해야한다. 

그리고 Linked Anim Graph를 통해  ABP_Echo에 사용한다. 이때 변수들은 ABP_Echo의 변수가 바인딩될 수 있도록

디테일 창에서 바인딩 시켜준다.

 

이제 IK부분을 추출하여 애니메이션 블루프린트를 만들어주자 

이때 변수들은 바인딩시켜주고 캐시포즈같은경우에는 Input Pose를 통해 처리해주면 된다.

이렇게 하면 이전과 동일한 애니메이션이 출력되는 것을 볼 수 있다.

 

 

오늘부터는 충돌과 오버랩 이벤트에 대해서 알아보자

 

모든 충돌이 가능한 객체는 충돌 섹션이 있다. 그리고 오늘 여기서 주의 깊게 볼 것은 콜리전 프리셋이다.

콜리전 프리셋은 많은 선택지를 가지고 있는데 이를 통해 해당 구성요소에 대한 충돌 시의 행동을 결정한다.

콜리선 반응에 대한 자세한 설정은 밑의 사진을 보면 알 수 있는데

각 선택지마다 밑의 선택지의 체크가 달라지고 이에 따라 다양한 콜리전 시의 행동을 할 수 있다.

 

만약 콜리전 활성화됨을 no collision으로 하게 되면 물체를 통과하게 된다.

 

이제 오버랩이벤트를 한번 보자 

일단 아이템에 플레이어와 충돌했을 때 이벤트가 발생하도록 오버랩을 수정해주자

 

그렇게하고 이벤트를 설정해주게 되면 아래 사진과 같은 결과가 나온다.

 

OnComponentBeginOverlap 이벤트는 특정 콜리전 컴포넌트가 다른 콜리전 컴포넌트와 충돌하거나 겹쳐지기 시작할 때 발생하는 이벤트로 주로 충돌을 감지하고, 이에 대한 반응을 프로그래밍하는 데 사용한다.

 

이것을 코드로 구현해보자

 

일단 콜리션 구 변수를 선언해주고 초기화 해주자

Item.h

class USphereComponent;

UPROPERTY(VisibleAnywhere)
USphereComponent* Sphere;

 

Item.cpp

#include "Components/SphereComponent.h"

Sphere = CreateDefaultSubobject<USphereComponent>(TEXT("Sphere"));
Sphere->SetupAttachment(GetRootComponent());

 

그리고 오버랩이벤트를 감지했을 때 작동할 Delegate에 바인딩할 콜백함수를 정의해주자

Item.h

	UFUNCTION()
	void OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);

 

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

	Sphere->OnComponentBeginOverlap.AddDynamic(this, &AItem::OnSphereOverlap);
}

void AItem::OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	const FString OtherActorName = OtherActor->GetName();
	if (GEngine)
	{
		GEngine->AddOnScreenDebugMessage(1, 30.f, FColor::Blue, OtherActorName);
	}
}

 

결과

 

마지막으로 콜리션에서 빠져나올 때 이벤트가 실행되는 OnSphereEndOverlap을 구현해보자 

일단 헤더파일에 콜백함수를 만들어주고 cpp코드의 BeginPlay에서 Delegate와 바인딩시켜준 뒤 원하는 기능을 콜백 함수

에 붙여주면 된다.

중요한 점은 리플렉션 시스템에 이용할 수 있도록 함수는 UFUNCTION()매크로 함수를 붙여주어야한다는 것이다.

Item.h

	UFUNCTION()
	void OnSphereEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);

Item.cpp

void AItem::BeginPlay()
{
	Super::BeginPlay();

	Sphere->OnComponentBeginOverlap.AddDynamic(this, &AItem::OnSphereOverlap);
	Sphere->OnComponentEndOverlap.AddDynamic(this, &AItem::OnSphereEndOverlap);
}

void AItem::OnSphereEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
	const FString OtherActorName = FString("Ending Overlap with : ") + OtherActor->GetName();
	if (GEngine)
	{
		GEngine->AddOnScreenDebugMessage(1, 30.f, FColor::Red, OtherActorName);
	}
}

 

결과

 

오늘부터는 애니메이션을 캐릭터 클래스에 넣어보자 

지금은 매시에 애니메이션 모드를 Use Animation Asset으로 설정해두고 Anim을 Idle로 해둬서 이 애니메이션이 고정된다.

 

일단 애니메이션/애니메이션 블루프린트를 만들어주자 이때 Skeleton은 캐릭터의 블루프린트의 스켈레톤을 사용해주자

이때 애니메이션 블루프린트에는 Anim Graph이 있다.

Event Graph는 논리적 실행흐름을 다루고 Anim Graph는 캐릭터의 포즈 정보를 다룬다. 사진에 보이는 Result에 

포즈 정보를 넘겨주면 캐릭터의 애니메이션이 바뀌게 된다. 

Anim Graph

이제 매쉬를의 애니메이션 모드를 Use Animation Blueprint로 하고 만든 애니메이션 블루프린트로 지정해주자

그리고 애니메이션 블루프린트에서 Character를 참조할 수 있는 변수를 추가해주자 지금은 null포인터이다. 

따라서 이 변수를 초기화해주는 과정이 필요하다

Event Graph

애니메이션 블루프린트의 이벤트 그래프를 보면 두가지가 기본적으로 있는 것을 볼 수 있는데 Try Get Pawn Owner은 이 애니메이션 블루프린트를 사용하는 객체를 가져올 수 있는 것이고 Blueprint Update Animation은 Tick 이벤트와 비슷하게 작동하는 것이다.

 

이제 Try Get Pawn Owner와 Blueprint Initialize Animation 이벤트를 통해 캐릭터 객체와 그 캐릭터의 움직임에 대한 변수인 Character movement Component를 초기화해주자

객체 초기화

그리고 플레이어가 움직이고 있는지를 확인하기 위해 매 프레임마다 Velocity Vector의 x y 요소의 길이를 가지고 와서 초기화 해주자

 

 

이 변수를 사용할 수 있도록 Anim Graph에서 State Machine을 만들어주자 

State Machine 내부는 Idle과 Run State가 Transition으로 변경될 수 있도록 한다. 이때 Transition은 

Ground Speed가 0보다 클 때 Run으로 0일 때 Idle로 가게 만들어준다.

 

그렇게 해주면 이제 뛰거나 멈춰있는 애니메이션이 재생되는 것을 볼 수 있다.

 

 

이제 코드로 구현해보자 

일단 Character.cpp 옆에 AnimInstance를  상속받는 클래스를 생성해주자 

 

그리고 헤더파일에서 캐릭터와 무브먼트 변수를 전방선언해주고 속도는 float 변수로 선언해주자 

public:
	virtual void NativeInitializeAnimation() override;
	virtual void NativeUpdateAnimation(float DeltaTime) override;

	UPROPERTY(BlueprintReadOnly)
	class ASlashCharacter* SlashCharacter;

	UPROPERTY(BlueprintReadOnly, Category = Movement)
	class UCharacterMovementComponent* SlashCharacterMovement;

	UPROPERTY(BlueprintReadOnly,Category=Movement)
	float GroundSpeed;
};

 

그 다음 캐릭터 포인터변수를 초기화 해줘야한다.

그전에 C++에서의 Cast를 알아보자 

static_cast - 컴파일 시간에 실행된다.

dynamic_cast - 런타임에 타입 변환을 수행하고 다형성을 지원

언리얼- Cast 사용

우리는 언리얼의 Cast를 사용할 것이다. 

이를 통해 초기화해주고 무브먼트 변수또한 초기화해주자 

그리고 GroundSpeed는 KismetMathLibrary 라이브러리의  UKismetMathLibrary::VSizeXY함수를 통해 계산되도록 하자

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


#include "Characters/SlashAnimInstance.h"
#include "Characters/SlashCharacter.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "Kismet/KismetMathLibrary.h"

void USlashAnimInstance::NativeInitializeAnimation()
{
	Super::NativeInitializeAnimation();

	SlashCharacter = Cast<ASlashCharacter>(TryGetPawnOwner());
	if (SlashCharacter)
	{
		SlashCharacterMovement = SlashCharacter->GetCharacterMovement();
	}
}

void USlashAnimInstance::NativeUpdateAnimation(float DeltaTime)
{
	Super::NativeUpdateAnimation(DeltaTime);

	if (SlashCharacterMovement)
	{
		GroundSpeed = UKismetMathLibrary::VSizeXY(SlashCharacterMovement->Velocity);
	}
}

 

이렇게 해주면 코드를 통해서 캐릭터의 애니메이션이 변하는 것을 볼 수 있다.

 

이제 캐릭터가 점프할 수 있도록 만들어보자 

점프는 액션매핑을 해준 다음 코드상으로 일단 Character에 내장되어있는 Jump 함수를 사용하도록 하자

SlashCharacter.h

UPROPERTY(EditAnywhere, Category = Input)
UInputAction* JumpAction;

SlashCharacter.cpp

EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Triggered, this, &ACharacter::Jump);

 

여기에 애니메이션을 추가해주자

플레이어가 떨어지고 있는지 확인해줄 변수를 추가하고 매틱마다 그것을 확인할 수 있도록하자

SlashAnimInstance.h

UPROPERTY(BlueprintReadOnly, Category = Movement)
bool IsFalling;

SlashAnimInstance.cpp

void USlashAnimInstance::NativeUpdateAnimation(float DeltaTime)
{
	Super::NativeUpdateAnimation(DeltaTime);

	if (SlashCharacterMovement)
	{
		GroundSpeed = UKismetMathLibrary::VSizeXY(SlashCharacterMovement->Velocity);
		IsFalling = SlashCharacterMovement->IsFalling();
	}
}

 

그리고 멈춰있는 것과 뛰는 모션을 제외한 주요한 움직임에 관한 애니메이션을 모아두기위해

하나의 State Machine을 추가해주자

그리고 기존에 사용하던 Ground Locomotion을 캐쉬포즈로 저장하여 다른 State Machine에서 사용할 수 있도록 하자

 

전체흐름도는 아래와 같이 잡고 Transition은 IsFalling을 통해 이루어지도록한다. 

여기서 Land -> OnGround 는 Transition을 선택하고 해당 속성을 클릭해준다.

이렇게하면 Jump애니메이션이 작동은 한다. 하지만 원래 OnGround로 돌아오는 것이 너무 느리다.

이때 Land -> OnGround 트랜지션 조건을 추가하고 여기에 Get Relevant Anim Time과 Ground Speed의 bool 연산으로 실행시간이 일정초를 넘고 걷고  있다면 OnGround로 넘어가게 했다.

 

잘 동작하는 것을 볼 수 있다.

 

마지막으로 만약 캐릭터가 경사진 곳으로 움직이게 된다면 발이 공중에 뜨게 되는데 이때 Inverse Kinematic을 

사용한다. 

예를 들어 왼쪽 보다는 오른쪽 캐릭터가 자연스럽다. 이때 캐릭터의 본을 사용하는데 이때 Inverse Kinematic을 통해 계산을 하게된다.

 

이때 어느 발을 구부려하는 등의 정보는 발에서 디버그 구체를 쏘는 방식으로 결정하게 된다. 

 

이제 실제로 만들어보자 

일단 애니메이션/컨트롤릭/컨트롤릭 을 생성해주자

그리고 캐릭터의 뼈를 가져 import해주자 우리가 여기서 사용할 것은 발부분의 가상 뼈나 IK 뼈를 사용할 것이다.

 

일단 발의 위치에서 일정구간 구로 Trace해서 Trace가 Hit되는 곳의 좌표를 벡터로 반환해주는 함수를 만들어주자

1. 이를 활용하여 왼발 오른발의 Hit 포인트의 Z값을 저장하고 이를 활용해보자

그리고 보간을 통해서 뼈를 이동시킬 것인데 이때 사용할 변수도 추가해주자 

2. 보간을 통해 값이 뼈의 이동이 자연스럽게 이루어지도록 하자 

3. 골반의 위치는 왼쪽 오른쪽 중에 작은 것의 위치로 지정해주자 

 

4.이 값이 IK 발 쪽 뼈에 적용되도록 하자.

 

5. IK의 값이 실제 뼈에 적용되도록 하자

이제 애니메이션 블루프린트에서 실제로 사용하면 된다. 이때 Blend 기능과 bool을 통해 떨어지고 있을때와 이동하고 

있을 때를 제외했을 때 IK기능이 작동하도록 하면 된다.

 

이렇게하면 IK가 잘 작동하는 것을 볼 수 있다.

 

오늘은 이제 캐릭터를 만들어 볼 것이다

 

일단 캐릭터 C++클래스부터 만들어보자 

 

그리고 이를 활용한 블루프린트클래스도 만들어주자 만들게 되면 기본적으로 캡슐컴포넌트와 매쉬등이 만들어져있다.

이때 매쉬에서 캐릭터매쉬를 하나 지정해주고 애니메이션도 Idle로 하나 넣어주자

 

그리고 이제 움직일 수 있게 입력액션과 입력매핑컨텍스트를 넣어주자 

입력액션에서는 좌우로도 움직일 수 있게 Vector2D로 입력을 받도록하자.

입력액션

입력컨텍스트에서는 앞뒤 좌우로 움직일 수 있게 wasd로 매핑해줄 것인데 이때 ad는 x축으로 이동하는것으로 그대로 두면 되지만 ws는 y축으로 움직이는 것으로 Modifiers에 스위질 입력축 값과 negate를 추가해주어야한다. 

 

이제 이에 맞는 코드를 작성해주자 

이전에 했던 것과 동일하게 매핑컨텍스트와 액션매핑 및 콜백함수를 헤더파일에서 정의하고 

BeginPlay에서 입력컨텍스트를 추가해준다음 그 입력컨텍스트에 함수들을 붙여주고 

함수를 정의해주면 된다.

그리고 FInputActionValue를 헤더에서 사용할 때는 헤더파일을 generated.h 전에 include 시켜주어야한다.

 

Slash.h

	#include "InputActionValue.h"
    
	class UInputMappingContext;
	class UInputAction;
private:
	UPROPERTY(EditAnywhere,Category = Input)
	UInputMappingContext* SlashContext;

	UPROPERTY(EditAnywhere,Category = Input)
	UInputAction* MovementAction;
    
    void Move(const FInputActionValue& Value);

 

Slash.cpp

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

}

void ASlashCharacter::Move(const FInputActionValue& Value)
{
	const FVector2D MovementVector = Value.Get<FVector2D>();

	//보는곳으로 이동할 수 있게
	const FRotator Rotation = Controller->GetControlRotation();
	const FRotator YawRotation(0.f, Rotation.Yaw, 0.f);
	
	//보는 방향가져오기
	const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
	AddMovementInput(ForwardDirection, MovementVector.Y);
	const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
	AddMovementInput(RightDirection, MovementVector.X);
}

void ASlashCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

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

}

 

 

핫리로딩해준 다음 블루프린트에서 해당 입력 칸을 채워주면 된다.

결과

이제 카메라추가를 해보자

전방선언을 통해 스프링암과 카메라를 추가해주자

Slash.h

class USpringArmComponent;
class UCameraComponent;

UPROPERTY(VisibleAnywhere)
USpringArmComponent* SpringArm;

UPROPERTY(VisibleAnywhere)
UCameraComponent* ViewCamera;

 

Slash.cpp

#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"

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

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

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

 

핫리로딩을 해주면 카메라와 스프링암이 생긴것을 볼 수 있다.

 

이제 캐릭터에 머리와 눈썹을 달아주자 

이때 눈썹과 머리 에셋이 로드가 안될 수도 있는 데 이때는 groom에 해당하는 2개의 플러그인은 체크해주고 재시작하면 

로드가 잘된다.


코드상으로 머리와 눈썹 변수를 생성해주자 

일단 모듈을 가져올 수 있도록 빌드.cs 코드에 "HairStrandsCore", "Niagara" 두개를 추가해준다. 그리고 

Binaries Intermediate Saved 이 3개의 파일을 지운 뒤 uproject파일을 generate해준다. 

그리고 다시 들어가서 빌드해주면 모듈을 가져올 수 있다.

 

이제 헤더파일에 전방선언을 통해 UGroomComponent 포인터 객체를 2개 선언해주자

class UGroomComponent;
    
UPROPERTY(VisibleAnywhere,Category=Hair)
UGroomComponent* Hair;

UPROPERTY(VisibleAnywhere,Category = Hair)
UGroomComponent* Eyebrows;

 

그리고 생성자에 변수를 초기화해주고 소켓부분을 만들어주자

	//소켓지정
	Hair->AttachmentName = FString("head");
	
	Eyebrows = CreateDefaultSubobject<UGroomComponent>(TEXT("Eyebrows"));
	Eyebrows->SetupAttachment(GetMesh());
	//소캣지정
	Eyebrows->AttachmentName = FString("head");

 

그리고 컴파일해주면 Eyebrows와 Hair가 메시 하위에 생긴 것을 볼 수 있다. 그리고 그룸컴포넌트는 GroomAsset을 넣을 수 있는 곳이 있는데 여기에 눈썹과 머리를 넣어주면 된다.

 

이렇게하면 머리와 눈썹을 달 수 있다!

https://school.programmers.co.kr/learn/courses/30/lessons/12902

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 

문제를 봤을 때 DP로 풀면 좋을 것 같다고 생각을 했다. 그러려면 일반 점화식을  도출해내야한다. 

문제를 생각해보면 짝수에서만 가능하다는 것을 알 수 있다. 그리고 일반적인 경우를 생각해보자면

 

  • 인 경우:
    • 타일을 채울 수 없는 경우로, 한 가지 방법이 존재-> 아무것도 하지 않는 방법.
  • 인 경우:
    • 가로로 타일 3개 배치
    • 세로로 2개를 위로 쌓아 배치
    • 세로로 2개를 아래로 쌓아 배치 
  • 인 경우:
    • 크기의 바닥을 두 번 채우는 경우와 유사하게 생각할 수 있다.

이제 점화식을 도출해보자 

크기의 바닥을 채우는 방법을 찾기 위해 혹은 그 이하의 크기에서 타일을 어떻게 배치하는지 생각해보자

n의 경우의 수를 구하려면 일단 n-2번째와 n-4.. 0이 될때까지의 경우의 수를 모두 더 해주어야한다. 이때 n-4의 패턴부터는 좌우 대칭적으로 배치할 수 있기 때문에 곱하기2를 해준다.

코드로 구현하면 다음과 같다

#include <string>
#include <vector>
#define MOD 1000000007
using namespace std;
//dp활용 
//짝수일때만 계산가능

int solution(int n) {
   if (n % 2 != 0) return 0; // 홀수일 경우 0 반환
    
    vector<long long> dp(n + 1, 0);
    dp[0] = 1; // base case
    dp[2] = 3; // 기본적인 2x3 타일 배치 방법의 수
    
    for (int i = 4; i <= n; i += 2) {
        dp[i] = dp[i - 2] * dp[2];
        for (int j = i - 4; j >= 0; j -= 2) {
            dp[i] += dp[j] * 2;
        }
        dp[i] %= MOD;
    }
    
    return dp[n];
}

 

 

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

 

일단 새를 시점으로 플레이할 수 있도록 새의 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을 조정하여 자연스럽게 날 수 있도록 조정하자

https://school.programmers.co.kr/learn/courses/30/lessons/87377

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 

 

교점을 찾아서 사각형 격자판을 만들어야하는 문제이다.

어떻게 교점을 찾는지가 핵심이다.

이때는 행렬 방정식을 이용해보도록 하자.

#include <string>
#include <vector>
#include <set>
#include <algorithm>
#include <climits>

using namespace std;

vector<string> solution(vector<vector<int>> line) {
    set<pair<int, int>> points;          //교점 저장할 변수

    //교점계산
    for (int i = 0; i < line.size(); i++) {
        for (int j = i + 1; j < line.size(); j++) {
            long long a1 = line[i][0], b1 = line[i][1], c1 = line[i][2];
            long long a2 = line[j][0], b2 = line[j][1], c2 = line[j][2];
            long long inc = a1 * b2 - a2 * b1;              //행렬식
            if (inc == 0) continue;

            //교점계산
            long long x_num = b1 * c2 - b2 * c1;
            long long y_num = a2 * c1 - a1 * c2;

            //정수부분인지 계산
            if (x_num % inc == 0 && y_num % inc == 0) {
                int x = x_num / inc;
                int y = y_num / inc;
                points.insert({ x,y });
            }
        }
    }

    //격자판범위계산
    int min_x = INT_MAX, max_x = INT_MIN;
    int min_y = INT_MAX, max_y = INT_MIN;
    for (auto point : points) {
        min_x = min(min_x, point.first);
        max_x = max(max_x, point.first);
        min_y = min(min_y, point.second);
        max_y = max(max_y, point.second);
    }

    //격자판만들기
    int wid = max_x - min_x + 1;
    int height = max_y - min_y + 1;
    vector<string> grid(height, string(wid, '.'));            //.으로 격자판초기화
    for (auto point : points) {
        int x = point.first - min_x;            //x좌표 계산
        int y = max_y - point.second;           //y좌표 계산
        grid[y][x] = '*';
    }
    return grid;
}

+ Recent posts