http://gamearchitect.net/2008/06/01/an-anatomy-of-despair-aggregation-over-inheritance/
김학규가 번역한 버전:
Despair 프로젝트의 초기 디자인을 하면서 나와 아드리안이 결정한 사항중 하나는 '가급적 상속보다 조합을 우선시하자'라는 점이었다. 이것은 내가 처음 생각해낸 것은 아니다. "조합 상속" 같은 단어로 구글을 검색해보면 수백만가지 결과를 찾아볼 수 있다. C++ 개발자 커뮤니티에서도 지난 몇년간 상속 개념의 용도가 불필요하게 많이 과장되었다고 회고하는 경향이 있다. Sutter 와 Alexandrescu 같은 저자들은 C++ 코딩 스탠다드라는 자신들의 책에 아예 '상속보다는 조합을 쓰라'는 가이드라인까지 정해놓았다.
그럼에도 불구하고 우리가 Despair 전에 작업했던 모든 게임 엔진들에는 비슷한 깊은 상속의 계층구조가 자리잡고 있었으며 90년대 중반에는 이런 구조가 유행이었다. '플레이어' 클래스는 '전투원' 클래스의 한 종류로 상속받았고, '전투원'은 다시 '이동체'에서, 그것은 다시 '물체' 에서, 그것은 다시 '베이스 개체' 에서 상속받는 식이다.
이런 구조에는 여러가지 약점들이 존재한다. 그중 몇가지를 열거해보자면:
첫째, 융통성이 부족하다. 만약 새로운 AI 를 가진 적을 만들면서 적 A 의 기능 몇가지와 적 B 의 기능 몇가지를 조합하고 싶다면 이런 문제는 상속구조로 간단히 표현하기가 어렵다. 이상적인 게임엔진이라면 디자이너가 프로그래머에게 의존하지 않고도 새로운 AI 들을 조합해낼 수 있어야 할 것이다. 하지만 깊게 구성된 상속구조에서는 항상 프로그래머가 관여하여 별로 좋지 않은 대안들 중에서 하나를 억지로 선택해야만 한다. 그 대안들의 예를 들자면, 새로운 클래스를 상속시키고 기존 코드들로부터 코드를 복사하던가, 아니면 다중상속구조를 쓰면서 '혹시 다이아몬드 형으로 꼬이진 않겠지'라고 바랄 수 밖에 없는 식이다.
둘째, 계층 구조에 있는 클래스들은 게임 개발이 지속되면서 그 복잡도가 통제할 수 없을 정도로 불어나게 된다. 만약 플레이어 클래스가 오브젝트 계층구조의 일부라면, 이 클래스에 입력과 조작 시스템 관련 부분, 커스텀 애니메이션 표현 부분, 아이템을 줍고 인벤토리를 관리하는 부분, 적을 타게팅을 보조하는 부분과 네트워크 정보를 동기화하는 부분, 그 외에 게임에 특화된 부분들을 만들어넣어야 한다. 우리가 전에 작업했던 한 게임에서는 플레이어 클래스의 구현이 13,000 라인이 넘어갔고, 플레이어 클래스는 12,000 가지의 물리적인 오브젝트 클래스로부터 상속받았다 (역자주 : 어떤 식으로 상속했길래 ...) 그런 파일들에선 무슨 내용을 찾기도 어렵고, 여러사람이 같이 작업하다보면 커밋하다가 충돌이 나는 부분이 많이 생겼다
세째, 깊게 구성된 상속 계층은 물리적(소스코드를 컴파일 하는 컴파일러의 입장)으로 볼때 별로 좋지 않은 구조이다. 클래스 A 가 B 로 부터 상속받고, B 는 C 로부터 상속받는다고 하면 A.h 는 B.h 와 C.h 를 포함해야만 한다. 상속 계층이 깊어지면 깊어질 수록, 더더욱 많은 직접 관련이 없는 헤더 파일을 포함해야만 하고, 컴파일 시간은 기하급수적으로 느려지기 마련이다. 코드간의 관계가 느슨해질 수록 컴파일 시간은 빨라질 수 있다 (존 라코스의 저서 Large scale C++ design 에서 이에 관련된 내용들을 참고하기 바람)
그래서 우리는 Despair 를 최대한 콤포넌트화하기로 했고, 상속구조도 가급적 평평하게 만들기로 했다. Despair 에 등장하는 오브젝트는 기본적으로 유니크ID 와 게임 콤포넌트를 담은 컨테이너를 래핑한 것에 불과하다. 컴포넌트는 그 타입 정보로 쿼리하고 동적으로 캐스팅해서 조작할 수 있다. 게임 오브젝트가 담당하는 부분은 오직 게임 오브젝트가 언제 소멸될 것인가에 대한 부분과 아이디를 관리하는 것뿐이다. 게임 오브젝트는 자기가 소유한 컴포넌트의 구현에 대해서는 알지 못하며, 일반적인 게임 오브젝트가 갖고 있는 위치, 충돌범위, 모양등에 대한 정보는 갖고 있지 않다.
이 방식으로 다른 시스템들도 구성하였다. 우리의 Scene 오브젝트의 구현도 게임 오브젝트와 유사하게 구현하여, 하나의 Scene 오브젝트가 각각의 화면에 등장할 Scene 노드들의 모음에 대한 활동주기를 관리하게 하였다. Scene 노드들은 각각의 계층구조를 통해 Render state 와 Skeletal transform 을 관리한다.
이런 시스템들은 또한 비쥬얼 에디팅을 위한 flow-graph 라이브러리 상으로도 구현되었다. 게임 로직, 애니메이션, 재질들을 프로그래머가 아닌 사람들도 그래프를 조작하는 것으로 연결할 수 있는 툴이 마련되었다.
우리에게 상속 대신 조합을 사용하는 방식은 매우 잘 작동한다. 이 시스템을 디자인하면서 가장 걱정한 사항중 하나는 혹시 실행속도가 떨어지는 것은 아닐까 하는 점이었다. 현재로서는 깊은 상속구조를 사용했던 예전보다 실행속도의 차이가 나타난 징후는 보이지 않는다. 설령 차이가 난다고 해도, 오히려 더 좋은 쪽으로 차이가 날 수도 있다고 추측한다. 왜냐하면 각 컴포넌트들이 잘 캡슐화되어 있기 때문에, 관련 코드들은 메모리상 가까이에 있게 되고, 캐쉬 locality 에 긍정적인 영향을 줄 수 있기 때문이다. 또한 각 컴포넌트중 변화가 생긴 부분에 대해서만 업데이트를 할 수 있기 때문에, 프레임 률에 따라 선별적인 업데이트를 하는 것으로 최적화할 여지도 있기 때문이다.
만약 다시 처음부터 시스템을 디자인할 기회가 생겨서 오브젝트 조합에 대한 부분을 바꾸게 된다면, scene 오브젝트를 game 오브젝트와 달리 좀 더 내부를 드러내지 않도록 할 생각이다. Scene 오브젝트에 주어진 문제는 다른 오브젝트와 차이가 있다. 현재 게임 오브젝트용 컴포넌트는 수백가지가 만들어졌으며, 가벼운 오브젝트 인터페이스를 통해 컴포넌트의 타입을 얻어와 조합하는 방식의 융통성은 상당한 잇점을 안겨주었다. 내 생각에는 게임 오브젝트 시스템은 게임 플레이를 위해 빨리 컴포넌트를 추가해서 조립하는 점에서는 이상 해결책에 도달했다고 본다. 하지만 Scene 오브젝트는 좀 경우가 다르다. Scene 라이브러리를 만든 이후로 Scene 노드 타입은 추가된 적이 없으며, 모든 Scene 노드들은 상위 레벨 코드보다는 Scene 라이브러리 내부에서 구현되고 있다. 한편으로는 Skeletal 계층구조를 재계산 하는 부분에 대해 다양한 최적화를 시도해보면서 상위 레벨 코드들은 그대로 유지할 수 있도록 인터페이스를 유지하는 것이 바람직할 것이다. 이런 점들로 하여금, 디미터의 법칙을 따르고, Scene 오브젝트의 구현을 감추는 것이 Game 오브젝트를 금방 콤포넌트를 조립해 만들고 테스트하는 환경에는 바람직하지 않겠지만 Scene 오브젝트에게는 더 적절하게 적용될 수 있게 만든다.
이론적으로 완벽한 소프트웨어의 세계에서와는 달리 사람이라는 요소가 결부된 실무의 세계에서는 콤포넌트 기반의 디자인을 적용하게 되면 몇가지 당황스러운 점들이 생겨난다. 신입 프로그래머들은 콤포넌트 기반의 엔진을 사용하기 위해 더 가파른 학습곡선을 넘어가야만 한다. 몇가지 중요 함수들을 거치다보면 전체 시스템의 큰 흐름을 이해할 수 있었던 예전 구조에 비해, 새로운 시스템에서는 계속 들어가도 들어가도 핵심이 금방 보이지 않아 혼동을 겪게 된다. 예를 들면 Despair 에는 이름에 '콤포넌트'라는 단어가 들어간 클래스가 500 개가 넘고 object 라는 이름이 들어간 클래스가 100 개가 넘는다. 우리의 게임은 C++ 오브젝트에 의해 정의되는 것이 아니라, 오브젝트간의 관계에 의해 정의 된다. 그 관계를 이해하기 위해서는 예전보다 훨씬 문서화와 커뮤니케이션의 비중이 커질 수 밖에 없다.
컴포넌트 기반으로 가면서 얻은 또 다른 교훈은, 컴포넌트 기반의 디자인에서는 코드에 있던 복잡도를 데이타로 옮겨놓게 되는 경향이 있다. 디자이너들은 오브젝트를 디자인하고 상속구조를 구성하는데 익숙해있지가 않다. 컴포넌트를 갖고 작업하게 되면 새로운 프로세스와 좋은 사람들을 필요로 하게 된다. 상호교류는 필수적이다. 프로그래머들은 컴포넌트용 코드를 작성하는 것 외에도, 툴에 넣을 오브젝트를 만들기 위한 작업을 해야 하고, 테크니컬 디자이너들과 공동으로 작업하면서 피드백을 받고, 디자이너들에게 더 많은 재료들을 제공해야만 한다. 게임과 같이 팀은 각각의 멤버에 의해 정의되는 것이 아니라, 멤버간의 관계에 의해 정의되는 것이다
An Anatomy of Despair: Aggregation Over Inheritance an
해부학의 절망 : 집계 이상 상속
One of the first decisions that Adrian and I made in our initial work on Despair was to prefer aggregation to inheritance whenever possible. This is not an original idea. If you Google for “aggregation inheritance” or “composition inheritance,” you’ll get a million hits. The C++ development community has been renouncing its irrational exuberance over inheritance for the last few years now. Sutter and Alexandrescu even include “prefer composition to inheritance” as a guideline in C++ Coding Standards . 아드리안 중 하나를 먼저 결정하고 그 작업을 만들었어요 절망의 초기 집계를 상속을 선호하는가 가능하다면합니다. 이것은 단순한 독창적인 아이디어를합니다.면 구글에서 "점착력이있는 상속"또는 "조성 상속,"당신 얻을 백만 안타합니다. the c + + 개발 커뮤니티가 상속 포기 그 이상의 불합리한 지난 몇 년 동안, 지금 풍부합니다. 셔터 및 alexandrescu도 포함 "선호와 구성 성분이 상속"라는 지침에 c + + 코딩 표준합니다.
Nonetheless, every game engine we’d worked on before Despair had a similar deep inheritance hierarchy of the sort that was in vogue in the mid-nineties: a player class might inherit from some kind of combatant class, which would inherit from a mover class, which would inherit from a physical object class, which would inherit from a base game object class. 그럼에도 불구하고, 모든 게임 엔진에 근무하기 전에 우리는 비슷한가 깊은 절망에 있던 상속 계층 구조를 정렬 - 1990 년대 중반에 유행 : 한 선수 클래스가 어떤 종류의 전투에서 상속 클래스, 어느 것이 이동 클래스에서 상속 로 할 것이며, 신체적인 객체 클래스에서 상속, 어느 것이 기반 게임 객체 클래스에서 상속합니다.
This architecture has a lot of shortcomings. Let me enumerate a few of them: 이 아키텍처의 결점이 많이있습니다. 제가 몇 사람은 그들을 열거 :
First, it’s inflexible. If you want to create a new AI enemy that has some of the capabilities of enemy A and some of the capabilities of enemy B, that’s not a task that fits naturally into a hierarchical object classification. Ideally, you’d like the designers to be able to create a new enemy type without involving you, the programmer, at all. But with a deep object hierarchy, you have to get involved and you have to try to pick the best implementation from several bad options: to have your new enemy class inherit from one object and cut-and-paste the functions you need from the other; to not inherit from either, and to cut-and-paste the functions you need from both; or to tiptoe down the treacherous slippery slope of multiple inheritance and hope that it doesn’t lead to a diamond of death . 첫째로, 조금 더 융통성합니다. 싶다면 사랑을 새로 만들려면이 중 일부의 기능을 적군과 일부의 기능을 적을 적을 b, 그 작업은 자연에 맞는 개체의 계층 구조로 분류합니다. 이상 면, 그냥 같은 디자이너를 입력하지 않고 적을 수있습니다 관련된을 새로 만들려면 당신은 프로그래머, 전혀합니다. 그러나 깊은 개체를 계층 구조, 당신은 관계를 갖기로하고 최고의 구현을 선택해야 몇몇 시도 잘못된 옵션 :이 당신의 새로운 적을 클래스에서 상속 - 1 개체를 잘라내기 - 붙여넣기 기능이 필요하고 다른 세계에서 온;하지 않는 중 하나에서 상속과를 잘라 -와 -를 붙여 넣습니다에서 필요한 기능을 둘 다;이나 발꼬락 언덕 아래로 미끄러 복수의 위험한 유산과 희망을 리드하지 않는 것으로 확인되었습니다 다이아몬드의 사망합니다.
Second, a handful of classes in your hierarchy tend to grow without bound over a game’s development. If the player class is part of the object hierarchy, then you can expect this class to include input and control systems, custom animation controls, pickup and inventory systems, targeting assistance, network synchronization–plus any special systems required by the particular game that you’re making. One previous game that we worked on features a 13,000 line player class implementation, and the player class inherited from a 12,000 physical object class. It’s hard to find anything in files that size, and they’re frequent spots for merge conflicts since everyone’s trying to add new stuff to them all the time. 둘째로, 귀하의 계층에서 소수의 수업 경향이 게임의 개발을 통해 성장을 구속하지 않고있습니다. 경우에 해당 개체 플레이어 클래스는 계층 구조의 일부를 기대하실 수있습니다 입력 및 제어 시스템이 클래스를 포함, 사용자 지정 애니메이션 컨트롤, 픽업 및 재고 시스템, 타겟팅을 지원, 네트워크 동기화 - 플러스 특별한 시스템의 특정 게임을 당신이 그렇게 만들 필요로합니다. 하나 이전에 우리가 공부하는 게임을 구현 클래스에는 13000 선 플레이어와 플레이어 클래스 12000 물리적 개체에서 상속된 클래스합니다. 그것은 파일의 크기를 찾기 어려운 사람은 아무도, 그들은 모든 사람의 이후로 병합 충돌이 잦은 관광 명소에 대한 새로운 컨텐츠를 추가하려는 사람들은 늘합니다.
Third, deep inheritance is poor physical structure. If class A inherits from class B which inherits from class C, then the header file for A–Ah–has to #include Bh and Ch As your hierarchy gets deeper, you’ll find that all of your leaf classes have to include four or five extra headers for their base classes at different levels. For most modern games, as much compile time is spent opening and reading header files as is spent actually compiling code. The more loosely your code is coupled, the faster you can compile. (See Lakos for more details.) 셋째, 깊은 상속이 가난한 물리적 구조를합니다. 만약 클래스에서 상속 a 상속 클래스 b 어떤 종류 c, 다음의 헤더 파일에 - 아 -가 # 채널을 포함하고 귀하의 계층 구조가 더 깊은 bh 찾을 수있을 것입니다 모든 귀하의 나뭇잎이 클래스는 4 개 또는 5 개의 여분을 포함 헤더를 서로 다른 수준의 기본 과목을 강의하고있습니다. 대부분의 현대적인 게임, 한 많은 시간을 소비하는 경우가 개방과 독서 헤더 파일을 컴파일을 소비하는 경우가 사실은 컴파일 코드로합니다. 더 느슨한 결합 귀하의 코드는 이 속도를 컴파일하실 수있습니다. (lakos 대한 자세한 내용합니다.)
Therefore we resolved to make Despair as component-based as we could, and to keep our inheritance hierarchies as flat as possible. A game object in Despair, for example, is basically a thin wrapper around a UID and a standard vector of game components. Components can be queried for type information and dynamically cast. The game object provides only lifetime management and identifier scoping. It knows nothing about component implementations. It contains no traditional game object state like position, bounds, or visual representation. 따라서 우리를 절망으로 구성 요소 - 기반으로 해결 할 수있습니다, 그리고 우리의 유산을 유지 가능한 평면 계층 구조로합니다. 게임 개체를 절망, 예를 들면, 주위에 래퍼는 기본적으로 얇은 uid와 표준 벡터의 게임 구성 요소를합니다. 구성 요소를 동적으로 질의를 할수있다 캐스트에 대한 정보를 입력합니다. 게임 객체를 제공 범위에서만 수명 관리 및 식별자입니다. 그것에 대해 아무것도 모르고 구성 요소를 구현합니다. 그것 포함되지 전통적인 게임 객체의 상태처럼 위치, 경계, 또는 시각적 표현합니다.
This approach has informed other systems as well. Our scene object implementation is similar to the game object implementation, with a single object representing each model that provides lifetime management for a vector of scene nodes. Scene nodes manage their own hierarchies for render state or skeletal transforms. 이러한 접근 방식은 정보를 다른 시스템도합니다.의 현장 개체를 구현은 유사하게 게임 개체를 구현, 각 모델을 제공하는 하나의 개체를 대표하는 벡터의 현장을 평생 관리 노드를합니다. 장면 노드를 자신의 계층 구조를 관리하는 상태 또는 골격을 렌더링 변환합니다.
Another family of systems is built on a flow-graph library for visual editing. Game logic, animation systems, and materials can all be built by non-programmers wiring together graph components in the appropriate tools. 또 다른 가족이 시스템은 라이브러리에 대한 그래프 기반의 흐름 - 영상 편집합니다. 게임 로직, 애니메이션 시스템과 물자를 모두가 함께 기본 차트 구성 요소가 아닌 - 프로그래머 배선의 적절한 도구를합니다.
Using composition instead of inheritance has worked very well for us. Our primary concern when we set out in this new direction was that we’d end up with something that had horrible runtime performance. With Fracture almost complete, though, there’s no evidence that our performance is worse than it would have been with a deep inheritance hierarchy. If anything, I’m inclined to suspect that it’s better, since well-encapsulated components have better cache locality than large objects and since the fact that we only update dirty components each frame means that we can decide what does and doesn’t need to be updated at a finer granularity. 상속를 사용하여 구성 대신에 우리가 일을 잘합니다. 우리의 주된 관심사는 우리가 새로운 방향 설정이 의자에서 것에서부터는 우리가 결국은 무서운 런타임 성능을합니다.과 골절 거의 완료 그러나 증거가 없다 우리의 성능은이 됐을 것보다 더 나쁜 유산과 계층 구조에 깊은합니다.이 생긴다면, 나는 그것이 더 나은 것으로 의심되는 성향이 때문에 음 - 캡슐화된 구성 요소가 큰 개체와보다 더 나은 캐시 위치는 사실을 우리는 단지 이후 각 프레임 구성 요소를 업데이 트를 더러운 것인지 결정할 수있습니다 무엇을 의미하고 세밀하게하지 않습니다 단위를 업데이 트해야합니다.
If I were starting over again, the only change I’d make with respect to object composition is to make scene objects more opaque and less like game objects. Scene objects have a different problem to solve. We have several hundred game components now, with more going in all the time, and the flexibility of having a thin game object interface that allows querying components for type has paid big dividends. I think that our game object system is close to ideal for iterating rapidly on gameplay. Scene objects are a different kind of problem, though. We haven’t added any new scene node types since the scene library was written, and all scene nodes are implemented in the scene library instead of in higher-level code. At the same time, it would be nice to be able to experiment with different optimizations of updating skeletal hierarchies without breaking higher-level code. All of this argues that following the Law of Demeter and hiding scene object implementation details would have been appropriate for scene objects even if it wasn’t appropriate in the rapid-prototyping environment of game objects. 만약 그렇다면 다시 시작의 유일한 변경과 관련하여 개체를 사귀는 게 좋아 장면 개체를 구성은 게임처럼 더 적은 개체를 불투명하게하고있습니다. 장면 개체가 다른 문제를 해결합니다. 우리는 수백 개의 게임 구성 요소를 지금,과 더 많은 들어간다의 모든 시간과 게임의 유연성을 객체 인터페이스가 얇은 데 쿼리 구성 요소의 유형은 유료 큰 배당을 사용합니다. 나는 우리 게임 개체를 생각하는 시스템은 빠른 속도를 반복 게임 주변에 이상적입니다. 장면 개체는 다른 종류의 문제가지만합니다. 우리는 어떤 새로운 장면이 추가되지 않았습니다 현장 라이브러리가 작성된 이후 노드 유형, 그리고 도서관의 모든 장면 장면에서 구현 대신에 노드가 높은 - 수준의 코드합니다. 동시에, 그것이 좋겠 네요 서로 다른 최적화로 실험을 할 수 골격의 업데이트 계층 구조를 파괴하지 않고 더 높은 - 수준의 코드합니다.이 모든 것이 다음과 같은 주장의 법칙을 데메테르 개체를 구현 세부 정보를 숨기는 장면 장면에 적합 개체를했을 경우에도 적합한 아니었 환경의 급속한 - 프로토 타입의 게임 개체를합니다.
Beyond the perfect abstract world of software architecture, component based design also created a couple of surprises in the messier world of development process and human interaction. One lesson of working with a composition-based engine is that the learning curve for new programmers is steeper. For programmers who are used to being able to step through a few big nested functions and see the whole game, it can be disorienting to step through the game and discover that there’s just no there there. For example, Despair contains over 500 classes with the word “component” in their names and 100 classes with the word “object” in their names. Our games aren’t defined by C++ objects, they’re defined by relationships between them. To understand those relationships, you need good documentation and communication more than ever. 완벽한 추상적인 세계를 넘어 소프트웨어 아키텍처, 구성 요소 기반의 디자인 또한 놀라움을 몇 개 만들어 메시에 세계의 개발 과정과 인간의 상호 작용을합니다. 하나의 교훈 작업하는 작곡 - 기반 엔진은 새로운 프로그래머가 급격한 학습 곡선을합니다. 사람은 프로그래머를 사용하여 몇 수 있다는게 큰 중첩 기능과보고를 통해 단계를 전체 게임, disorienting 수있습니다 게임 및 발견을 통해 단계로가 단지 거기서없습니다. 예를 들어, 수업 절망 포함 500 개 이상 이라는 단어가 포함된 "구성 요소"이라는 단어가 포함된 그들의 이름과 100 클래스 "개체"자신의 이름을합니다. 저희 c + + 개체를 정의하는 게임이 없다, 그들은 그들 사이의 관계에 의해 정의됩니다. 그들을 이해 관계, 좋은 설명서가 필요 와 통신 어느 때보 다도합니다.
Another composition lesson is that component-based design takes a lot of the 또 다른 구성 레슨은 해당 구성 요소 - 기반의 디자인은 많은 complexity that used to exist in code and pushes it into data. Designers aren’t used to designing objects and constructing inheritance hierarchies. Working with components requires new processes and good people. Cross-pollination is important. Your programmers need to work building objects in your tools, as well as writing code for components, and they need to work hand-in-hand with good technical designers who can provide tool feedback and build on the object primitives available to them. Like a game, your team isn’t defined by its individual components but by the relationships between them. 복잡도에 존재하는 코드를 사용하여 데이터를 밀어 넣어합니다. 설계자는 사용되지 않음을 디자인 개체 및 상속 계층 구조를 구축합니다. 작업과 구성 요소를 새 프로세스와 좋은 사람이 필요합니다. 크로스 - 수분이 중요합니다. 귀하의 프로그래머의 객체에 구축해야 할 필요가 당신의 도구를 쓰는 코드를 구성 요소뿐만 아니라, 그들은해야 할 필요가 손 -이 - 손가 좋은 기술적인 디자이너를 만들 수있는 개체를 기본 제공 도구를 피드백과 그들을 사용할 수있습니다. 같은 게임, 귀하의 팀은이 정의되지 않음 개별 구성 요소를 거치지 않고는 그들 사이의 관계.