| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 | 31 |
- Shimon Schocken
- 생능출판
- 주우석
- 전공자를 위한 C언어 프로그래밍
- C++
- (주)책만
- 메타버스
- The Elements of Computing Systems 2/E
- 김진홍 옮김
- 밑바닥부터 만드는 컴퓨팅 시스템 2판
- 이득우
- Noam Nisan
- hanbit.co.kr
- 입출력과 사칙연산
- unity6
- 일기
- 잡생각 정리글
- JavaScript
- C
- https://insightbook.co.kr/
- HANBIT Academy
- 게임 수학
- 데이터 통신과 컴퓨터 네트워크
- booksr.co.kr
- 박기현
- 알고리즘
- BOJ
- 이득우의 게임수학
- 백준
- C#
- Today
- Total
cyphen156
Transform 활용 방법에 대한 고민 본문
Transform은 오브젝트가 공간 안에서 자신의 위치와 회전, 크기를 표현할 수 있게 해준다.
Unity의 GameObject가 Transform을 가지며, Unreal Engine의 Actor도 Root Component를 통해 공간상의 Transform을 표현한다.
CyphenEngine 역시 World에 소속된 WorldObject는 반드시 Transform을 가져야 한다는 규칙을 전제로 삼았다.
문제는 Transform을 어디에 저장하고, 누가 변경하며, 어떤 방식으로 처리해야 하는가에 있다.
이는 단순히 자료구조 하나를 선택하는 문제가 아니다. 객체 중심의 행위와 데이터 지향 실행 사이에서 기준을 정해야 하는 문제다.
객체가 자신의 Transform을 다루는 방식
객체 중심 구조에서는 Object가 자신의 Transform을 직접 다루기 쉽다.
Player.Update()
→ 입력 확인
→ Transform 변경
→ 변경된 상태로 후속 행위 실행
플레이어가 입력을 받고 이동한 뒤, 변경된 위치를 이용해 충돌이나 상호작용을 판정한다.
코드가 실행된 순서와 게임 안에서 행위가 발생한 순서를 일치시키기 쉽고,
특정 상태를 변경한 직후 그 결과를 다시 사용하는 것도 자연스럽다.
문제는 다수의 객체를 처리할 때 발생한다.
각 Object는 서로 다른 Component 구성을 가질 수 있으며,
Object와 Transform이 메모리에 연속적으로 배치된다는 보장도 없다.
Object A → Transform A
Object B → Transform B
Object C → Transform C
객체 하나를 중심으로 보면 자연스러운 구조지만,
Transform 수천 개를 처리해야 하는 System의 입장에서는 불규칙한 메모리 접근이 된다.
System은 Object의 다른 Component가 아니라 Transform만 연속적으로 읽고 계산하고 싶기 때문이다.
모든 Transform이 같은 방식으로 처리되지는 않는다
월드에는 직접 행위를 거의 하지 않거나 완전히 고정된 대상도 대량으로 존재한다.
Terrain, 지형지물, 식생, 반복 배치된 구조물 역시 공간을 구성하므로 Transform 또는 그에 준하는 공간 표현이 필요하다.
하지만 이들의 위치는 대부분 변하지 않거나 제한된 규칙으로만 변경된다.
따라서 모든 대상을 객체별 Update에서 처리할 필요는 없다.
Static Transform
한 번 계산하고 결과 유지
Dirty Transform
변경된 대상만 재계산
Active Transform
현재 System이 처리할 대상만 일괄 계산
Transform이 많이 존재한다는 이유만으로 모든 Transform이 Hot Data인 것도 아니다.
Transform이 100만 개 있어도 대부분 고정되어 이번 프레임에 읽히지 않는다면 Hot Path에 있지 않다.
반대로 Transform이 100개뿐이어도 Physics, Animation, Hierarchy, Rendering이 반복해서 사용한다면 해당 실행 데이터는 Hot Data가 된다.
Transform이라는 자료형 자체보다 어떤 Transform을, 어느 System이, 어느 시점에 처리하는지가 중요하다.
World의 원본과 System의 실행 데이터
World에는 오브젝트의 현재 공간 상태를 나타내는 논리 Transform이 존재한다.
하지만 모든 System이 이 원본을 같은 형태와 순서로 처리할 필요는 없다.
World Transform
여러 Tick 동안 유지되는 현재 공간 상태
WorldSnapshot의 복원 대상
Movement Working Set
이번 Tick에 이동할 Position과 Velocity
Physics Working Set
물리 계산에 참여할 Pose와 Collider
Render Frame
화면 생성에 필요한 WorldMatrix와 Bounds
Movement는 이동할 대상만 필요하고, Physics는 강체와 충돌에 참여하는 대상만 필요하다.
Renderer는 현재 화면을 만드는 데 필요한 데이터만 소비한다.
따라서 World의 원본 상태는 안정적으로 유지하고, 각 System은 현재 실행에 필요한 대상만 Working Set으로 구성할 수 있다.
World Persistent State
→ 활성 상태와 이벤트 확인
→ System Working Set 구성
→ Batch Process
→ Resolve
→ Apply
Working Set이 항상 World 데이터를 복사해야 하는 것은 아니다.
System이 단독으로 사용하는 Hot Data는 실행 구조에 직접 둘 수 있고,
여러 Object와 System이 공유하는 상태는 World를 정본으로 유지한 채 현재 저장 위치만 연결할 수 있다.
논리 상태의 정본과 실행을 위한 물리적 표현은 같을 필요가 없다는 것이다.
DOD의 핵심은 ECS 자체가 아니다
ECS를 사용한다고 자동으로 데이터 지향적인 실행이 만들어지는 것은 아니다.
Component를 배열에 넣더라도 실제 실행 순서와 무관하게 데이터가 뒤섞여 있다면,
매 요소마다 분기와 불규칙한 접근이 발생한다.
반대로 객체 구조를 유지하더라도,
실행 단계에서 필요한 데이터만 추출하고 같은 규칙을 가진 대상으로 묶으면,
데이터 지향적인 처리가 가능하다.
World Object
→ 실행 데이터 추출
→ 같은 규칙의 데이터 구성
→ Batch Process
DOD의 핵심은 모든 데이터를 영구적으로 연속 배열에 넣는 것이 아니라,
현재 실행에 필요한 데이터를 처리 순서에 맞는 Working Set으로 구성하는 것이다.
모든 Transform을 System에서 처리하면 되는가
Transform은 Physics, Animation, Hierarchy, Rendering 등 여러 System에서 반복적으로 사용된다.
그렇다면 모든 Transform 변경을 System에 넘기는 것이 더 효율적이지 않을까?
사용자 정의 오브젝트에 대해서는 아니오라고 판단한다.
기술적으로 불가능한 것은 아니지만,
모든 변경을 균일한 System 실행 단계로 넘기면 처리량을 얻는 대신 객체 행위의 즉시성과 인과관계를 제한하게 된다.
여기서 말하는 정확성은 부동소수점 연산의 정밀도가 아니다.
사용자가 발생시킨 행위 순서에 따라 기대되는 계산 결과의 정확성을 의미한다.
1. B가 방어를 시작한다.
2. A가 B를 공격한다.
3. B가 방어를 해제한다.
객체 중심의 즉시 실행에서는 사건 순서를 그대로 표현할 수 있다.
B.Guard()
A.Attack(B) // 방어 중이므로 공격이 막힌다.
B.Unguard()
하지만 이를 데이터 종류별 System으로 나누면 실행 순서가 달라질 수 있다.
GuardSystem.ProcessAll()
GuardReleaseSystem.ProcessAll()
AttackSystem.ProcessAll()
각 System은 정상적으로 계산했지만 공격 처리 시점에는 B의 방어가 이미 해제되어 있다.
계산 자체가 실패한 것은 아니지만,
실제 행위 순서에 대해 사용자가 기대한 결과와 달라졌다.
그래서 순서에 따라 결과가 달라지는 연산은 데이터 처리에 유리하다는 이유만으로 자유롭게 재배치할 수 없다.
원래 사건 순서를 보존하려면
이벤트에 순서를 부여하고 대상의 현재 상태를 찾아가며 차례대로 적용해야 한다.
같은 대상에 접근하는 연산은 직렬화해야 하고,
그 과정에서 불규칙한 접근과 역참조 비용이 다시 발생한다.
인과 순서 보존
↕
데이터 접근 순서 재배치
↕
일괄 처리와 병렬화
세 가지를 항상 동시에 최대화할 수는 없다.
Physics와 Transform의 관계
Physics는 대량의 강체와 충돌을 일괄 계산하기 때문에 DOD의 이점을 얻기 좋다.
하지만 Physics 결과가
공격, 방어, 피격, 사망 같은 게임플레이 사건으로 전환되는 순간
다시 순서가 중요해진다.
Physics Process
→ Contact Result
→ Gameplay Resolve
→ Transform Apply
Physics Solver가 계산 중인 Pose를 Transform 원본에 바로 노출해서도 안 된다.
Physics가 최종 결과를 확정한 뒤 명시적인 Apply 단계에서 World의 Transform에 반영해야 한다.
Static
Transform → Physics
Kinematic
객체의 이동 목표 → Physics → 최종 Transform
Dynamic
Physics → 최종 Transform
객체가 Transform을 가진다는 사실과 그 Transform을 항상 객체가 직접 변경한다는 것은 같은 의미가 아니다.
대상의 성질과 현재 실행 단계에 따라 변경 권한이 달라질 수 있다.
객체 종류가 아니라 연산 성질로 구분한다
OOP와 DOD의 적용 범위를 Player와 Bullet 같은 객체 종류로 나누는 것은 정확하지 않다.
하나의 Player 안에도 서로 다른 실행 성질이 존재하기 때문이다.
Player
입력 해석
객체 행위
공격과 방어 판정
순서 보장 이벤트
이동 적분
System 일괄 처리
WorldMatrix 생성
System 파생 데이터
RenderItem 생성
렌더 실행 표현
순서와 인과관계가 중요한 연산은 객체 또는 Ordered Event로 처리하고,
재배열 가능한 대량 계산은 DOD System으로 처리한다.
이렇게 구성한다면 같은 Object 안에서도 두 실행 경로는 병존할 수 있다.
CyphenEngine의 World
CyphenEngine의 World는 Object 메모리를 소유하는 컨테이너가 아니다.
WorldObject가 World에 소속된다는 것은 소유권을 넘긴다는 뜻이 아니라,
어느 논리 공간의 Transform, Physics, 공간 질의, System에 참여하는지를 결정하는 Simulation Membership으로 정의하기로 했다.
World는 여러 Tick에 걸쳐 유지되는 현재 Simulation State이며,
WorldSnapshot을 만들기 위한 논리 공간의 정본이다.
동시에 자신에게 소속된 ObjectUpdate와 System Loop를 가진다.
World
ObjectUpdate
SystemSync
SystemCompute
Resolve
Apply
FrameExtract
일반적인 WorldObject는 소속 World의 시간축과 ObjectUpdate에 참여한다.
하지만 Terrain이나 정적 지형물처럼 객체 행위가 필요하지 않은 대상은
ObjectUpdate에 참여하지 않고 필요한 System에만 등록될 수 있다.
일반 WorldObject
World Membership
ObjectUpdate
필요한 System 참여
Static WorldObject
World Membership
ObjectUpdate 없음
필요한 System 참여
World Membership과 ObjectUpdate 참여는 분리할 수 있지만,
일반적인 객체는 소속 World의 Update에 참여하는 것을 기본으로 한다.
World와 무관한 Update가 필요한 대상
모든 실행 대상이 반드시 특정 World의 시간축에 속한다고 단정할 수는 없다.
여러 World 사이의 관계를 조율하거나,
World가 생성되기 전이나 제거된 후에도 유지되어야 하는 기능이 생길 수 있다.
Editor와 Tool의 독립 실행, Runtime 전체의 상태 관리, Cross-World Coordination과 같은 World ObjectUpdate만으로 표현하기 어려운 대상을 예시로 들 수 있다.
World Update Domain
WorldObject
World-local System
World 시간과 Snapshot 적용
Independent Update Domain
Runtime 조율
Editor와 Tool
Cross-World Coordination
이러한 실행축을 지금 모든 Object가 사용할 수 있는 범용 기능으로 확정할 필요는 없다.
다만 World-local Update만으로 표현할 수 없는 대상이 생길 가능성은 닫지 않기로 했다.
독립적인 Update가 실제로 필요해지면 실행 주체, 시간축, 상태 소유권, Snapshot 범위를 함께 정의해야 한다.
World Loop에서 OOP와 DOD가 만난다
ObjectUpdate
객체 행위
Command와 Event 생산
↓
SystemSync
Enroll과 Unenroll
↓
SystemCompute
Working Set 일괄 처리
↓
Resolve
상호작용 순서와 결과 확정
↓
Apply
World의 정본 상태에 반영
↓
FrameExtract
Render Frame 생성
ObjectUpdate는 객체 중심의 행위와 인과관계를 담당한다.
SystemCompute는 동일한 규칙을 공유하는 데이터를 일괄 처리한다.
Resolve는 계산 결과를 게임플레이 의미로 변환하며,
Apply는 확정된 결과를 World 상태에 반영한다.
두 실행 경로는 같은 World 안에 존재하지만 같은 방식으로 실행되지 않는다.
결론
Transform을 전부 OOP 또는 ECS 중 하나로 통일하는 것은 적절하지 않다고 판단을 내렸다.
Transform은 WorldObject의 논리적인 공간 표현이지만, 모든 Transform이 같은 빈도와 방식으로 처리되는 것은 아니기 때문이다.
그래서 WorldObject는 자신의 정체성인 ObjectHandle을 통해 World의 TransformStorage와 연결되도록 구성하기로 했다.
'프로젝트 > CyphenEngine' 카테고리의 다른 글
| CyphenEngine 개발기 #6 크로스플랫폼 : Linux로 포팅하기 (0) | 2026.07.03 |
|---|---|
| CyphenEngine 개발기 #5 Renderer 모듈화 : DirectX11로 이미지 렌더하기 (0) | 2026.07.03 |
| CyphenEngine 개발기 #4 엔진 구조 리팩토링 (0) | 2026.07.02 |
| 자체엔진 개발 중간 기록 : Dx11.dll -> Vulkan.dll 렌더러 모듈 교체 테스트 (0) | 2026.07.01 |
| 저장용 리눅스 명령어 (0) | 2026.06.29 |
