[DirectX11] Animation System
DirectX 11로 게임 엔진 아키텍처 만들기
게임 개발에 있어 애니메이션은 캐릭터와 오브젝트의 동작과 표현의 핵심을 이룹니다. DirectX 11 기반 게임 엔진에서 Animation 클래스와 Animator 컴포넌트의 구현은 개발자에게 다양한 애니메이션 조작 기능을 제공합니다. 이들은 각 게임 오브젝트의 애니메이션 시퀀스를 관리하며, 애니메이션의 플레이, 정지, 반복 등을 제어합니다.

참고강의 링크:
[게임 프로그래머 도약반] DirectX11 입문 강의 - 인프런
게임 프로그래머 공부에 있어서 필수적인 DirectX 11 지식을 초보자들의 눈높이에 맞춰 설명하는 강의입니다., [사진][사진] [사진] 게임 개발자는 Unreal, Unity만 사용할 줄 알면 되는 거 아닌가요? 엔
www.inflearn.com
애니메이션 시스템 구현
1. 애니메이션 데이터 구조 설계
Animation 클래스는 ResourceBase 클래스를 상속받아, 게임 엔진의 리소스 관리 체계 내에서 관리됩니다. 애니메이션은 여러 Keyframe들로 구성되며, 각 키 프레임은 애니메이션의 특정 시점에서 오브젝트의 상태를 정의합니다.
// 키 프레임 구조체: 애니메이션의 각 프레임 정보를 저장
struct Keyframe
{
Vec2 offset = Vec2{ 0.f, 0.f }; // 텍스처 내에서의 오프셋
Vec2 size = Vec2{ 0.f, 0.f }; // 프레임의 크기
float time = 0.f; // 이 프레임이 표시되는 시간
};
2, Animation 클래스의 핵심 기능
1) 리소스 로딩과 저장
Animation 클래스는 XML 파일 형태로 애니메이션 데이터를 로드하고 저장하는 기능을 제공합니다. Load 메서드는 XML 파일로부터 애니메이션 데이터를 읽어들여, 각 키 프레임을 클래스 내부에 저장합니다. 반대로 Save 메서드는 현재 애니메이션 데이터를 XML 파일로 출력합니다.
// XML 파일로부터 애니메이션 데이터 로딩
void Animation::Load(const wstring& path)
{
tinyxml2::XMLDocument doc;
string pathStr(path.begin(), path.end());
XMLError error = doc.LoadFile(pathStr.c_str());
assert(error == XMLError::XML_SUCCESS); // 파일 로딩 성공 확인
XmlElement* root = doc.FirstChildElement();
string nameStr = root->Attribute("Name");
_name = wstring(nameStr.begin(), nameStr.end()); // 애니메이션 이름 설정
_loop = root->BoolAttribute("Loop"); // 반복 여부 설정
_path = path; // 파일 경로 저장
// 키 프레임 데이터 로딩
XmlElement* node = root->FirstChildElement();
for (; node != nullptr; node = node->NextSiblingElement())
{
Keyframe keyframe;
keyframe.offset.x = node->FloatAttribute("OffsetX");
keyframe.offset.y = node->FloatAttribute("OffsetY");
keyframe.size.x = node->FloatAttribute("SizeX");
keyframe.size.y = node->FloatAttribute("SizeY");
keyframe.time = node->FloatAttribute("Time");
AddKeyframe(keyframe); // 키 프레임 추가
}
}
// 애니메이션 데이터를 XML 파일로 저장
void Animation::Save(const wstring& path)
{
tinyxml2::XMLDocument doc;
XMLElement* root = doc.NewElement("Animation");
doc.LinkEndChild(root);
string nameStr(GetName().begin(), GetName().end());
root->SetAttribute("Name", nameStr.c_str());
root->SetAttribute("Loop", _loop);
root->SetAttribute("TexturePath", "TODO"); // 텍스처 경로 TODO: 실제 텍스처 경로 저장 구현 필요
// 키 프레임 데이터 저장
for (const auto& keyframe : _keyframes)
{
XMLElement* node = doc.NewElement("Keyframe");
root->LinkEndChild(node);
node->SetAttribute("OffsetX", keyframe.offset.x);
node->SetAttribute("OffsetY", keyframe.offset.y);
node->SetAttribute("SizeX", keyframe.size.x);
node->SetAttribute("SizeY", keyframe.size.y);
node->SetAttribute("Time", keyframe.time);
}
string pathStr(path.begin(), path.end());
auto result = doc.SaveFile(pathStr.c_str());
assert(result == XMLError::XML_SUCCESS); // 파일 저장 성공 확인
}
2) Shader
쉐이더 코드는 정점 데이터를 입력 받아 텍스처 매핑과 애니메이션 처리를 거쳐 최종적으로 픽셀의 색상을 결정합니다. 정점 쉐이더에서는 모델의 위치, 뷰, 투영 변환을 통해 각 정점의 최종 위치를 계산하고, 텍스처 좌표에 대한 애니메이션 처리를 수행합니다. 픽셀 쉐이더에서는 계산된 텍스처 좌표를 바탕으로 텍스처에서 색상을 샘플링하여 화면에 표시할 픽셀의 색상을 결정합니다.
// 정점 입력 구조체: 메시의 각 정점에 대한 정보를 정의합니다.
struct VS_INPUT
{
float4 position : POSITION; // 정점의 위치
float2 uv : TEXCOORD; // 텍스처 좌표
};
// 정점 쉐이더 출력 구조체: 정점 쉐이더 처리 후의 데이터를 픽셀 쉐이더로 전달하기 위한 구조체입니다.
struct VS_OUTPUT
{
float4 position : SV_POSITION; // 스크린 좌표계에서의 정점 위치
float2 uv : TEXCOORD; // 텍스처 좌표
};
// 카메라 데이터 상수 버퍼: 뷰 매트릭스와 투영 매트릭스를 포함합니다.
cbuffer CameraData : register(b0)
{
row_major matrix matView; // 뷰 매트릭스
row_major matrix matProjection; // 투영 매트릭스
};
// 변환 데이터 상수 버퍼: 월드 매트릭스를 포함합니다.
cbuffer TransformData : register(b1)
{
row_major matrix matWorld; // 월드 매트릭스
};
// 애니메이션 데이터 상수 버퍼: 애니메이션을 위한 데이터를 포함합니다.
cbuffer AnimationData : register(b2)
{
float2 spriteOffset; // 스프라이트 오프셋
float2 spriteSize; // 스프라이트 크기
float2 textureSize; // 텍스처 크기
float useAnimation; // 애니메이션 사용 여부 (1.0f는 사용, 0.0f는 미사용)
};
// 정점 쉐이더: 입력된 정점 데이터를 기반으로 스크린 좌표계의 위치와 텍스처 좌표를 계산합니다.
VS_OUTPUT VS(VS_INPUT input)
{
VS_OUTPUT output;
// 월드, 뷰, 투영 매트릭스를 곱하여 스크린 좌표계의 위치를 계산합니다.
float4 position = mul(input.position, matWorld); // 월드 변환
position = mul(position, matView); // 뷰 변환
position = mul(position, matProjection); // 투영 변환
output.position = position;
output.uv = input.uv;
// 애니메이션 사용 시, 텍스처 좌표를 조정합니다.
if (useAnimation == 1.0f)
{
output.uv *= spriteSize / textureSize; // 스프라이트 크기에 따라 텍스처 좌표 조정
output.uv += spriteOffset / textureSize; // 스프라이트 오프셋에 따라 텍스처 좌표 조정
}
return output;
}
// 텍스처와 샘플러 상태
Texture2D texture0 : register(t0); // 텍스처
SamplerState sampler0 : register(s0); // 샘플러
// 픽셀 쉐이더: 정점 쉐이더로부터 받은 텍스처 좌표를 사용하여 최종 색상을 계산합니다.
float4 PS(VS_OUTPUT input) : SV_Target
{
float4 color = texture0.Sample(sampler0, input.uv); // 텍스처 샘플링
return color; // 계산된 색상 반환
}
Animator 클래스
애니메이션 클래스는 애니메이션을 재생, 일시 정지, 멈춤 등을 관리합니다. 각 게임 오브젝트는 애니메이션 인스턴스를 가지며, 게임 루프 동안 애니메이션 상태를 업데이트합니다.
1, 애니메이션 업데이트
Animator 클래스는 게임 루프의 각 프레임마다 애니메이션 상태를 업데이트합니다. 이 과정에서 현재 애니메이션의 키 프레임을 결정하고, 애니메이션의 진행에 따라 다음 키 프레임으로 전환합니다.
// Update 함수: 매 프레임마다 애니메이션 상태를 업데이트
void Animator::Update()
{
// 현재 애니메이션 객체 가져오기
shared_ptr<Animation> animation = GetCurrentAnimation();
if (!animation) return; // 애니메이션 객체가 없으면 업데이트 중단
// 현재 키 프레임 정보 가져오기
const Keyframe& keyframe = GetCurrentKeyframe();
// 경과 시간 업데이트
float deltaTime = TIME->GetDeltaTime();
_sumTime += deltaTime;
// 다음 키 프레임으로 전환 조건 체크
if (_sumTime >= keyframe.time)
{
_currentKeyframeIndex++; // 다음 키 프레임으로 인덱스 이동
int32 totalCount = animation->GetKeyframeCount(); // 전체 키 프레임 수
// 마지막 키 프레임을 넘어설 경우 처리
if (_currentKeyframeIndex >= totalCount)
{
if (animation->IsLoop())
_currentKeyframeIndex = 0; // 반복 설정이 true면 처음으로
else
_currentKeyframeIndex = totalCount - 1; // 반복이 아니면 마지막 키 프레임 유지
}
_sumTime = 0.f; // 키 프레임 시간 초기화
}
}
2, 애니메이션 및 키 프레임 관리
Animator 클래스는 현재 애니메이션과 키 프레임을 관리합니다. 이를 통해 개발자는 게임 오브젝트에 다양한 애니메이션을 적용하고, 게임의 상황에 따라 적절한 애니메이션을 재생할 수 있습니다.
전체코드
Animator.h
#pragma once
#include "Component.h" // 기본 컴포넌트 클래스를 상속받기 위함
#include "Animation.h" // Animation 클래스 사용을 위한 포함
// Animator 클래스: 개별 게임 오브젝트의 애니메이션을 관리
class Animator : public Component
{
using Super = Component; // 상위 클래스 별칭
public:
Animator(); // 생성자
virtual ~Animator(); // 소멸자
void Init(); // 컴포넌트 초기화 함수
void Update(); // 프레임마다 애니메이션 상태 업데이트 함수
shared_ptr<Animation> GetCurrentAnimation(); // 현재 애니메이션 객체 반환
const Keyframe& GetCurrentKeyframe(); // 현재 키 프레임 반환
void SetAnimation(shared_ptr<Animation> animation); // 애니메이션 설정 함수
private:
float _sumTime = 0.f; // 현재 키 프레임 진행 시간
int32 _currentKeyframeIndex = 0; // 현재 키 프레임 인덱스
shared_ptr<Animation> _currentAnimation; // 현재 애니메이션 객체
};
Animator.cpp
#include "pch.h"
#include "Animator.h"
#include "Game.h"
#include "TimeManager.h"
// 생성자: 컴포넌트 타입을 Animator로 설정
Animator::Animator() : Super(ComponentType::Animator)
{
}
// 소멸자: 추가적인 정리 필요시 구현
Animator::~Animator()
{
}
// Init 함수: 초기화 로직 구현
void Animator::Init()
{
}
// Update 함수: 매 프레임마다 애니메이션 상태를 업데이트
void Animator::Update()
{
// 현재 애니메이션 객체 가져오기
shared_ptr<Animation> animation = GetCurrentAnimation();
if (!animation) return; // 애니메이션 객체가 없으면 업데이트 중단
// 현재 키 프레임 정보 가져오기
const Keyframe& keyframe = GetCurrentKeyframe();
// 경과 시간 업데이트
float deltaTime = TIME->GetDeltaTime();
_sumTime += deltaTime;
// 다음 키 프레임으로 전환 조건 체크
if (_sumTime >= keyframe.time)
{
_currentKeyframeIndex++; // 다음 키 프레임으로 인덱스 이동
int32 totalCount = animation->GetKeyframeCount(); // 전체 키 프레임 수
// 마지막 키 프레임을 넘어설 경우 처리
if (_currentKeyframeIndex >= totalCount)
{
if (animation->IsLoop())
_currentKeyframeIndex = 0; // 반복 설정이 true면 처음으로
else
_currentKeyframeIndex = totalCount - 1; // 반복이 아니면 마지막 키 프레임 유지
}
_sumTime = 0.f; // 키 프레임 시간 초기화
}
}
// 현재 애니메이션 객체 반환
std::shared_ptr<Animation> Animator::GetCurrentAnimation()
{
return _currentAnimation;
}
// 현재 키 프레임 반환
const Keyframe& Animator::GetCurrentKeyframe()
{
return _currentAnimation->GetKeyframe(_currentKeyframeIndex);
}
Animation.h
#pragma once
#include "ResourceBase.h" // 기본 리소스 클래스를 상속받기 위함
// 키 프레임 구조체: 애니메이션의 각 프레임 정보를 저장
struct Keyframe
{
Vec2 offset = Vec2{ 0.f, 0.f }; // 텍스처 내에서의 오프셋
Vec2 size = Vec2{ 0.f, 0.f }; // 프레임의 크기
float time = 0.f; // 이 프레임이 표시되는 시간
};
class Texture; // Texture 클래스의 전방 선언
// Animation 클래스: 애니메이션 리소스 관리
class Animation : public ResourceBase // ResourceBase를 상속받음
{
using Super = ResourceBase; // 상위 클래스 타입 별칭
public:
Animation(); // 생성자
virtual ~Animation(); // 소멸자
// 리소스 로딩 및 저장
virtual void Load(const wstring& path) override;
virtual void Save(const wstring& path) override;
// 애니메이션 설정
void SetLoop(bool loop); // 애니메이션 반복 설정
bool IsLoop(); // 애니메이션이 반복되는지 확인
// 텍스처 관련 함수
void SetTexture(shared_ptr<Texture> texture); // 애니메이션에 사용될 텍스처 설정
shared_ptr<Texture> GetTexture(); // 현재 설정된 텍스처 반환
Vec2 GetTextureSize(); // 텍스처 크기 반환
// 키 프레임 관련 함수
const Keyframe& GetKeyframe(int32 index); // 특정 인덱스의 키 프레임 반환
int32 GetKeyframeCount(); // 키 프레임의 총 개수 반환
void AddKeyframe(const Keyframe& keyframe); // 새로운 키 프레임 추가
private:
bool _loop = false; // 애니메이션 반복 여부
shared_ptr<Texture> _texture; // 애니메이션에 사용될 텍스처
vector<Keyframe> _keyframes; // 키 프레임 목록
};
Animation.cpp
#include "pch.h"
#include "Animation.h"
#include "Texture.h"
Animation::Animation() : Super(ResourceType::Animation)
{
// 기본 리소스 타입을 Animation으로 설정
}
Animation::~Animation()
{
// 소멸자 구현
}
// XML 파일로부터 애니메이션 데이터 로딩
void Animation::Load(const wstring& path)
{
tinyxml2::XMLDocument doc;
string pathStr(path.begin(), path.end());
XMLError error = doc.LoadFile(pathStr.c_str());
assert(error == XMLError::XML_SUCCESS); // 파일 로딩 성공 확인
XmlElement* root = doc.FirstChildElement();
string nameStr = root->Attribute("Name");
_name = wstring(nameStr.begin(), nameStr.end()); // 애니메이션 이름 설정
_loop = root->BoolAttribute("Loop"); // 반복 여부 설정
_path = path; // 파일 경로 저장
// 키 프레임 데이터 로딩
XmlElement* node = root->FirstChildElement();
for (; node != nullptr; node = node->NextSiblingElement())
{
Keyframe keyframe;
keyframe.offset.x = node->FloatAttribute("OffsetX");
keyframe.offset.y = node->FloatAttribute("OffsetY");
keyframe.size.x = node->FloatAttribute("SizeX");
keyframe.size.y = node->FloatAttribute("SizeY");
keyframe.time = node->FloatAttribute("Time");
AddKeyframe(keyframe); // 키 프레임 추가
}
}
// 애니메이션 데이터를 XML 파일로 저장
void Animation::Save(const wstring& path)
{
tinyxml2::XMLDocument doc;
XMLElement* root = doc.NewElement("Animation");
doc.LinkEndChild(root);
string nameStr(GetName().begin(), GetName().end());
root->SetAttribute("Name", nameStr.c_str());
root->SetAttribute("Loop", _loop);
root->SetAttribute("TexturePath", "TODO"); // 텍스처 경로 TODO: 실제 텍스처 경로 저장 구현 필요
// 키 프레임 데이터 저장
for (const auto& keyframe : _keyframes)
{
XMLElement* node = doc.NewElement("Keyframe");
root->LinkEndChild(node);
node->SetAttribute("OffsetX", keyframe.offset.x);
node->SetAttribute("OffsetY", keyframe.offset.y);
node->SetAttribute("SizeX", keyframe.size.x);
node->SetAttribute("SizeY", keyframe.size.y);
node->SetAttribute("Time", keyframe.time);
}
string pathStr(path.begin(), path.end());
auto result = doc.SaveFile(pathStr.c_str());
assert(result == XMLError::XML_SUCCESS); // 파일 저장 성공 확인
}
// 텍스처 크기 반환
Vec2 Animation::GetTextureSize()
{
return _texture->GetSize();
}
// 지정된 인덱스의 키 프레임 반환
const Keyframe& Animation::GetKeyframe(int32 index)
{
return _keyframes[index];
}
// 키 프레임 개수 반환
int32 Animation::GetKeyframeCount()
{
return static_cast<int32>(_keyframes.size());
}
// 새로운 키 프레임 추가
void Animation::AddKeyframe(const Keyframe& keyframe)
{
_keyframes.push_back(keyframe);
}
RenderManager
// RenderObjects 함수: 수집된 게임 오브젝트들을 렌더링합니다.
void RenderManager::RenderObjects()
{
for (const shared_ptr<GameObject>& gameObject : _renderObjects) {
auto meshRenderer = gameObject->GetMeshRenderer();
if (!meshRenderer) continue; // 메시 렌더러가 없으면 무시
auto transform = gameObject->GetTransform();
if (!transform) continue; // 변환 컴포넌트가 없으면 무시
// 변환 데이터 설정 및 복사
_transformData.matWorld = transform->GetWorldMatrix();
PushTransfromData();
// 애니메이션 데이터 설정 및 복사 (해당되는 경우)
shared_ptr<Animator> animator = gameObject->GetAnimator();
if (animator)
{
const Keyframe& keyframe = animator->GetCurrentKeyframe();
_animationData.spriteOffset = keyframe.offset;
_animationData.spriteSize = keyframe.size;
_animationData.textureSize = animator->GetCurrentAnimation()->GetTextureSize();
_animationData.useAnimation = 1.f;
PushAnimationData();
_pipeline->SetConstantBuffer(2, SS_VertexShader, _animationBuffer);
_pipeline->SetTexture(0, SS_PixelShader, animator->GetCurrentAnimation()->GetTexture());
}
else
{
_animationData.spriteOffset = Vec2(0.f, 0.f);
_animationData.spriteSize = Vec2(0.f, 0.f);
_animationData.textureSize = Vec2(0.f, 0.f);
_animationData.useAnimation = 0.f;
PushAnimationData();
_pipeline->SetConstantBuffer(2, SS_VertexShader, _animationBuffer);
_pipeline->SetTexture(0, SS_PixelShader, meshRenderer->GetTexture());
}
PipelineInfo info;
info.inputLayout = meshRenderer->GetInputLayout();
info.vertexShader = meshRenderer->GetVertexShader();
info.pixelShader = meshRenderer->GetPixelShader();
info.resterizerstate = _rasterizerState;
info.blendState = _blendState;
_pipeline->UpdatePipeline(info);
_pipeline->SetVertexBuffer(meshRenderer->GetMesh()->GetVertexBuffer());
_pipeline->SetIndexBuffer(meshRenderer->GetMesh()->GetIndexBuffer());
_pipeline->SetConstantBuffer(0, SS_VertexShader, _cameraBuffer);
_pipeline->SetConstantBuffer(1, SS_VertexShader, _transformBuffer);
//_pipeline->SetTexture(0, SS_PixelShader, meshRenderer->GetTexture());
_pipeline->SetSamplerState(0, SS_PixelShader, _samplerState);
_pipeline->DrawIndexed(meshRenderer->GetMesh()->GetIndexBuffer()->GetCount(), 0, 0);
}
}
결론
DirectX 11을 사용한 게임 엔진 개발에서 애니메이션 시스템과 데이터 저장 클래스의 구현은 게임의 동적인 요소와 데이터 관리를 위해 중요합니다. 애니메이션은 게임 내 캐릭터와 오브젝트에 생동감을 불어넣어 주며, 데이터 저장 클래스는 게임의 중요한 정보를 관리하는 데 필수적입니다.