2010년 3월 15일 월요일

컴포넌트 시스템 정리 - 1

안녕하세요. 게임 프로그래밍은 기획자와 디자이너를 위한 서비스라고 생각하는 harry입니다. 요즘 온라인 게임이 유행하고 또 대형화 되면서 게임 개발 이후에 유지 보수 및 패치의 중요성이 대두 되고 있습니다. 그럼 쉽고 가볍게 패치를 하기 위해서는 게임 코딩에 어떤 걸 고려해야 할까요.
우선 컨텐츠로 인한 코드가 증가하여도 전체 클라이언트 어플리케이션 내의 복잡도가 증가하지 않아야 합니다. 그러나 이는 전통적인 오브젝트 - 오리엔트 프로그래밍에서는 쉽지 않습니다. 새 객체 추가 요청을 받았는데 기존과 어느 정도 비슷합니다. 그럼 기존 코드를 상속받아 새로운 코드를 작성합니다. 그러나 완전히 똑같질 않고 제거해야 되는 부분도 있습니다. 상속이므로 이미 사용되고 있는 기존 코드의 내용을 제거하기는 어렵습니다. 오버라이딩을 하게 됩니다.
그럼 컴포넌트 방식은 어떻게 될까요?
새롭게 추가 되는 내용을 머리 구상합니다. 이를 하나의 기능단위로 클래스로 만듭니다. 이 클래스는 상속으로 부터라기보다는 기본 컴포넌트 인터페이스로 부터 받습니다. 그리고 이 기능은 객체에 추가 됩니다.

쉽게 보면.
Human 라는 클래스가 있고,
여기에 CTransform(Translation,Rotation,Scale) 등의 정보를 갖는 클래스가 있습니다.

Human
  CTransform m_transform
  CAnimation m_animation

이러한 형태가 됩니다.

그러나 그냥 이렇게 코드를 짜게 되면
클래스가 늘어나게 되면 Entity 클래스가 복잡해지게 되고,

서로 다른 작동을 하는 사람 Entity와 나무 Entity는 각각 따로 클래스를 만들어야 됩니다.

그럼 어떻게 하면 될까요? 어떻게 하면 공통된 인터페이스로 처리할 수 있을까요?

Entity
  List<ComponentInterface*> m_componentList

ComponentInterface는 상속 받아서 CTransform,CAnimation 등의 컴포넌트를 가질 수 있습니다.

그럼 이렇게 했을 때는 기존의 경우와 비교해서 어떤 문제가 생길까요?

CAnimation은 캐릭터를 애니메이션 하기 위해서 Transform 정보가 필요합니다.

즉 CHuman::Update()
에서는 m_animation.Update(m_transform)
과 같이 m_animation 에 transform을 알려줄 필요가 생깁니다.

그럼 Entity 의 List 구조는 위와 같은 형태를 어떻게 해결할까요?
Entity 클래스는 리스트만 담고 있을 분 어떤 것들을 가지고 있는지 스스로 알 수 없습니다.

이런 경우에는

ComponentInterface 클래스에 다른 컴포넌트의 변수를 가지고 올 수 있는 인터페이스가 필요합니다.

보통 컴포넌트 간에 주고 받아야 하거나, 세이브/로드가 필요한 데이타의 경우 Property라고 부릅니다.
이 Property들을 주고 받으려면 인터페이스가 필요하게 됩니다.

즉 ReadProperty(Translate,&Vector3),ReadProperty(Rotation,&Vector3),ReadProperty(Scale,&Vector3)
과 같은 형태로 Transform을 읽어올 수 있도록 인터페이스를 만들 수 있습니다.

Entity
 ReadProperty(String,&Vector3)
    이 함수는 내부적으로 모든 프로퍼티를 돌면서 String에 해당하는 Property의 밸류를 찾아서 리턴 해줍니다.

물론 다른 컴포넌트의 값을 찾기 위해 String을 쓴다면, 프레임 저하가 생길것으로 예상됩니다.
스트링 비교는 매 프레임마다 하기에는 부하가 큰 연산입니다.
이럴 경우 스트링을 관리하는 클래스를 만들고,
String을 전체 테이블로 관리하게 하고 같은 이름의 스트링은 레퍼런스로 처리하면 됩니다.
같은 스트링의 경우는 같은  메모리 주소를 갖게 되므로 단순 비교로 처리할 수 있습니다.
C#의 StringBuilder등을 참조하세요.

그럼 Component Interface 를 어떻게 해야 할지 알아볼까요.
위에서 보았던 CTransform을 컴포넌트로 구성해 봅시다.
우선 ReadProperty 라는 함수가 필요합니다.
ReadProperty(string name,Point3& pt3)
 if(name == RotationString)
   pt3 = m_rotation;
 else if(name == ScaleString)
   pt3 = m_scale;

위와 같이 변수를 이름과 매핑 시켜서 가져옵니다. 상황에 따라서는 Map을 사용해서 빠르게 접근할 수 있도록 만들수 있습니다.

그럼 SetProperty 도 위와 같이 구성할 수 있습니다.
if...
else....

다른 컴포넌트에서 Entity의 ReadProperty를 호출하게 되면 모든 컴포넌트를 돌면서 해당하는
Property를 가지고 오게 됩니다.

그럼 다시 돌아가서 공통의 인터페이스를 가지는 컴포넌트를 만들어서
속성을 관리할 경우 어떤 이점이 있을까요?
우선 속성의 추가/삭제가 쉬워집니다.

게임을 개발하다 보면 기획에서 캐릭터가 새처럼 날게 해줘라고 어느날 갑자기 말합니다.
그래서 날기 기능을 열심히 기존 코드의 Update함수를 수정하여 넣습니다.
그리고 어느 날 아 나는 건 우리 게임에 어울리지 않고 유저들이 사용하지 않으니 빼달라고 합니다.

이미 기능을 추가한지 한참이 지났고 롤백으로 기능을 제거 할 수 없습니다. 이 경우 하나하나 찾아서 빼거나 최악의 경우
게으름을 이유로 호출하는 부분말 살짝 지웁니다.
Legacy 코드가 증가하게 되고, 결국 코드는 점점 방대해집니다.

그리고 컴포넌트로 했을 경우에는 어떨까요?
새처럼 날 수 있게 하는 FlyComponent가 추가 됩니다.
Entity의 ComponentList 에 추가 되겠지요.
그리고 어느 날 빼달라고 하면 그저 리스트에서 제거 됩니다.

그리고 두번째 장점은 직렬화-Serialization이 쉽게 가능해집니다.
스트링으로 매피되는 프로퍼티들을 처음에 추가 해주고,
GetProperty로 모두 얻어와 하나하나 저장할 수 있겠지요.

물론 이게 가능하려면 타입에 대한 어느 정도 구조화가 필요합니다.
즉 게임에서라면 자주 쓰이는 데이타인
INT,FLOAT,POINT3,MATRIX 등등의 기본 데이터가 있을테고,
이런 기본적인 데이타에 대해서는 어떻게 저장할지 미리 규칙을 정해둡니다.

그리고 이후 프로퍼티들은 위 규칙에 따라 매핑되는 이름으로 저장하게 됩니다.