------------------------------------2----------------------------------------
시네 카메라
슈팅게임에서 데미지를 입었을 때 화면 가장자리를 붉게 만들기
포스트 프로세스 볼륨
오픈 월드 게임에서 월드 전체에 포스트 프로세스 효과를 추가하고 싶을 때
영화적 기법
그레인 : 옛날 영화 필름효과 주기
비네팅 : 주변 어둡게
색수차 : 3색 분리 - 안티 앨리어싱의 효과로 사용 가능
bloom : 가우시안 블러 이용
렌즈 플레어 : 텍스처를 사용하는 이미지 기반 기법 화면 눈부시게 하는 효과
포스트 프로세스 머터리얼을 이용한 공포 영화 박동 효과 구현하기
포스트 프로세스 : 안티앨리어싱 함수 프로젝트 세팅에서 선택
-------------------------3--------------------------------------------
마스크 텍스처를 사용하여 하나의 머터리얼에 여러 재질을 표현하기
컬러 마스크
머터리얼 인스턴스 애셋 생성하여 적용해보기
ㄴ 기본 머터리얼 그래프가 같을 때 색상 러프니스 텍스처와 같은 것들을 변경하고 싶을 때
ㄴ 다이내믹 머터리얼 인스턴스를 사용하여 실행 중에 머터리얼 매개변수를 변경하기
작은 물체에 텍스처링하기
ㄴ Cheap Contrast, Custom Rotator 노드
모바일 플랫폼의 텍스처
ㄴ 최대크기 2K, 이미지의 변의 길이가 2의 승수이거나 크기가 정방형이어야 한다.
ㄴ 텍스처 Compression 속성을 Default나 Normal Map 옵션으로 설정 (메모리 사용량 줄이기)
플라스틱 질감의 천 표현하기
ㄴ Fresnel 노드
ㄴ Detail Texturing 노드 (엔진 컨텐츠 함수) (머터리얼의 질감을 향상시키기 위해 두 가지 다른 텍스처를 사용하는 노드)
ㄴ Texture Object 노드
반자동 절차적 머터리얼 생성하기
ㄴ 랜덤 패턴으로 둘 이상의 텍스처를 혼합하여 타일링이 덜 도드라져 보이게 한다.
ㄴ 최대한 다른 마스크를 사용
ㄴ Noise 노드 (앱의 성능 저하, 완전 자동 절차적 머터리얼, 무작위 패턴 생성)
머터리얼을 텍스처로 굽기
ㄴ 복잡한 원본 머터리얼 그래프의 결과를 단순한 텍스처에 구워서 원본
머터리얼 그래프의 복잡도를 제거하기
ㄴ Actor 블루프린트 (BP_MaterialBaker)-> DrawMaterialToRenderTarget 함수
ㄴ 베이킹할 머터리얼의 셰이딩 모델을 Unlit으로 변경하고 렌더 타겟에 그리기
ㄴ 렌더 타겟으로 텍스처 생성하기
+ 머터리얼의 성능문제 해결방법
ㄴ 1. 렌더 타겟의 해상도 조절로 해결
ㄴ 2. Merge Actors 툴을 활용하여 머터리얼을 단순한 버전으로 만들기
거리 기반 텍스처 혼합
ㄴ 머터리얼 그래프에서 카메라 위치와 정점 위치 거리를 이용하여
두 텍스처의 블렌드 알파 값을 계산
----------------------------------------4-----------------------------------------------
UV 채널 여러개?
SSR ( Screen Space Reflection ) 실시간 반사 효과
ㄴ 화면 공간 밖의 구역은 기하연산하지 못하다는 단점, 스크린 스페이스 바깥의
물체는 반사하지 못하기 때문에 그런 부분들에 fade out 기법을 사용할 수 있다.
ㄴ Ray Marching을 사용한다. 레이 트레이싱에 비해 가볍지만, 현실적인 그래픽
ㄴ 여러가지 버퍼를 사용해 GPU에서 연산, 화면 내부에 있는 정보로만 표현
SSAO ( Screen Space Ambient Occlusion)
Subsurface scattering 머터리얼(SSS) (빛이 반투명한 물체 표면에 투과되어 내부로 퍼져나간 후 다른 위치로 빠져나가는 현상)
왁스 재질 (사람 피부, 촛불)
머터리얼의 셰이딩 모델을 Subsurface Profile로 설정
Opacity -> 투명도가 아닌 서브서피스 효과의 강도를 나타냄
Metallic -> 사용하면 안됨
Subsurface Profile 애셋 필요
귓볼같은거
+ 서브서피스 효과를 가능하게 하는 셰이딩 모델
Standard Subsurface 모델
Preintegrated Skin 모델
Subsurface Profile 모델
ㄴ 스크린 공간 기법 (더 좋은 품질 제공 (비용 증가))
Two Sided Foliage 모델
완전한 투명 유리 재질 만들기
셰이딩 모델 Translucent + Default Lit
프레넬 값을 사용하여
Refraction (굴절) - 1. 굴절 지수, 2. 굴절모드 Pixel Normal Offset로 설정하기
Opacity (투명도)
Reflection (반사) 값들 보간
홀로그램 - (투명, 텍스처 애니메이션, 이미션)
셰이딩 모델 Translucent + UnLit (빛과 머터리얼이 상호작용하지 않음)
이미시브 입력을 상요하는 머터리얼을 설정하는 것이 조명을 대신할 수 있다.
ㄴ 디테일 -> Use Emissive for Static Lighting
실사같은 반사 표현하기
1. SSR
2. Reflection Capture
3. planar Reflections
ㄴ 프로젝트 세팅 > Support global clip for Planar Reflections
ㄴ Planar Reflection 액터를 사용 > 장면을 두번 렌더링 하기 때문에 기본적으로
비용이 많이 든다 대신 씬 캡처 기법
4. 씬 캡처 기법 (캡처링 리플렉션)
ㄴ 장면을 텍스처에 구워서 반사하는 물체에 제공
ㄴ Scene Capture Cube 액터를 사용 (카메라와 같음),
ㄴ 렌더 타깃은 씬 캡처 큐브에서 바라보는 장면을 저장하는 텍스처
ㄴ 거울 머터리얼이 렌더 타겟을 텍스처로 사용하면 됨
풀장의 물 표현하기
Translucent 셰이더 모델
ㄴ Depth Fade 노드 ( 깊이가 깊어질 수록 투과율이 떨어짐 ) - 투명도 설정에 사용
스크린 공간 계산에 의존하는 기법이기 때문에 뷰포트를 통해만 효과를 확인할 수 있다.
ㄴ Panner 노드 ( 좌표계, 시간, 속도 입력으로 받음)
+ Pixel Depth 함수, Scene Depth 함수
물 화선 표현
라이트 함수를 사용해 화선 효과를 표현
라이팅 채널 : 특정 표면에만 영향을 주는 특수한 효과를 만들고 싶을 때
물이 담긴 욕조 메시의 라이팅 채널을 수정
스포트 라이트에 머터리얼(라이트 함수)을 할당
애니메이션되는 바다 셰이더 만들기 (큰 규모의 표면)
Motion_4WayChaos_Normal 함수 ( 노말 텍스처를 기반으로 무작위 애니메이션 동작을 생성하는 함수)
----------------------------------------------5----------------------------------------------
레벨에 조명으로 에미시브 머터리얼 사용하기
ㄴ 조명 계산에 포함됨
ㄴ == 스태틱 조명 (동적 그림자 생성 X, 조명 빌드 필요)
ㄴ 월드 세팅 라이트매스 속성 조절
ㄴ 레벨에서 에미시브 머터리얼이 블룸에 영향을 주는 방법
1. 레벨 > Post Process Volume > Bloom 세기 조절
2. 머터리얼의 Emissive Boost 속성 조절
화면에 인터넷에서 받은 비디오 재생하기
미디어 플레이어 애셋
미디어 텍스쳐 애셋
스트림 미디어 소스 에셋
CCTV 카메라 만들기
액터(웹캠) 추가
static mesh 루트 컴포넌트로 만들고 하위에 SceneCaptureComponent2D 지정, 렌더 타깃 생성
이벤트 그래프에서 Timeline 노드 사용해서 static mesh 주기적으로 회전하게 만들기
게임에서 상호작용 가능한 물체 하이라이팅하기
포스트 프로세스 머터리얼
포스트 프로세싱 파이프라인에서 포스트 프로세스 머터리얼이 적용될 단계는 Before Tonemapping
합성곱 연산을 사용해 외곽선 판별을 수행한다.
Scene Texture : CustomDepth = 어떤 메시를 보고 싶은지 결정할 수 있도록 레벨에 있는 각 모델마다 활성화할 수 있는 속성
ㄴ Rendering 카테고리의 Render Custom Depth Pass 속성
Scene Depth = 카메라에 보이는 픽셀과 카메라 사이의 거리
모든 픽셀은 Scene Depth와 Custom Depth를 가진다. Custom Depth는 따로 설정하지 않으면 무한대의 값
게임 속 나침반 만들기
배그 0 ~ 360
머터리얼 블렌드 모드 Masked > Alpha 채널을 사용해 보이는 것과 보이지 않는 것을 조절할 수 있다.
UI 이벤트 그래프 > Tick > 이미지의 Dyanmic Material Get > PlayerRotation 파라미터에 값 설정
미니맵 만들기
오버레이 위젯
--------------------------------------------------6-----------------------------------------
정점 칠하기 기능으로 메시 칠하기
ㄴ 물체에 특정 효과를 텍스처에 직접 칠하지 않고 적용하고 싶을 때
Mesh Paint Mode로 메시 정점에 브러시로 색을 칠할 수 있다.
Vertex Color 노드에서 정점의 색을 딸 수 있다.
거대한 크기의 표면에서 종종 나타나는 반복 패턴을 제거하는데 사용
+ 텍스처 칠하기도 사용한다.
데칼 사용해 레벨에 세밀함 추가하기
1. 표준 디퍼드 데칼
2. 메시 데칼 - 프로젝트 세팅 필요 Rendering > Lighting > DBuffer Decals
데칼 처리과정
1. 머터리얼 구현
2. 머터리얼 투영
1. 디퍼드 데칼
ㄴ Material Domain Deferred Decal로 설정
ㄴ Decal Blend Mode Translucent
2. 메시 데칼
ㄴ CameraDirection Vector 노드를 사용하여 World Position Offset 조절
ㄴ 단순한 평면이 아니라 어떤 메시 위에도 텍스처를 투영할 수 있다.
시차 차폐 매핑을 사용한 벽돌 벽 표현하기
ㄴ 기하 셰이딩 같은 거? 실제로 픽셀을 위, 아래로 움직여서 노멀 맵으로 표현할 수 없는 진짜 3D 효과를 만들 수 있다.
ㄴ Parallax Occlusion Mapping 노드
ㄴ DitherTemporalAA 노드
ㄴ HeightMap?
ㄴ Pixel Depth Offset
디스플레이스먼트를 사용한 벽돌 벽
메시 디스턴스 필드를 사용한 거리 기반 마스킹
ㄴ 레벨에 있는 폴리곤 사이의 거리에 따라 머터리얼의 질감을 바꾸기
ㄴ 프로젝트 세팅 > 엔진 > 렌더링 > Lighting > Generate Mesh Distance Fields
ㄴ 동적이기 때문에 레벨에서 물체가 움직이면 효과가 자동으로 갱신된다.
-----------------------------------------7---------------------------------------
머터리얼 레이어링을 사용한 물체 위에 눈이 쌓인 효과
ㄴ 실험적인 기능, 기능 활성화 필요 (프로젝트 세팅 > 렌더링 > Support Material Layers)
ㄴ 추가적인 픽셀 셰이더 명령어가 필요 없어 성능 향상이 있다.
ㄴ Material Layer 에셋 사용, Make Material Attribute 노드
ㄴ 두개의 머터리얼 레이어 사용, 머터리얼 레이어를 사용하는 머터리얼
ㄴ 머터리얼에 Material Attribute 노드 추가 > 레이어, 백그라운드 레이어, 블렌드 에셋 (WorldAlignedBlend 노드는 어디에 입힐지 결정)
ㄴ 캐릭터 피부의 색상 스키마
커브 아틀라스를 통한 빠른 계절 변화
ㄴ Material Parameter Collection 에셋
ㄴ Curve Linear Color, Curve Atlas 에셋
ㄴ DeSaturation 노드, CurveAtlasRowParameter 노드, Blend_Overlay 노드
ㄴ 레벨 블루프린트에서 CreateDynamicMaterialInstance, SetScalarParameterValue, Delay 노드로
주기적으로 파라미터 (CurveAtlasRowParameter의 입력) 값을 바꿔준다. (0 ~ 1 사이)
랜드스케이프 머터리얼 혼합하기
ㄴ Landscape Layer Blend 노드 (랜드스케이프 모드에서 텍스처나 머터리얼을 레이어로 사용할 수 있다)
UV 커스터마이징
ㄴ 메인 노드의 CustomizedV UV0 속성
ㄴ 머터리얼 타일링과 애니메이션 구현에 좋음
----------------------------------------8-----------------------------------------
모바일 플랫폼 머터리얼 만들기
ㄴ 머터리얼 노드 > Fully Rough 체크하면 사용하는 명령어 개수 줄어듬
ㄴ Float 연산 정확도를 Full로 하면 비용이 많이 들기 때문에
ㄴ Use Lightmap Directionality 속성 : 노멀 맵 사용시 효과가 명확하게 보이기 위해 비용을 사용한다.
ㄴ Forward Shading 카테고리의 High Quality Reflections 속성 비활성화하기
반사를 표시하는데 사용할 큐브맵을 사용하지 않는다.
ㄴ 되도록 Default와 Unlit Shading 모델을 사용하고 투명이나 마스크 셰이딩 모델은 계산적인 측면에서 비싸므로
사용하는 머터리얼 수를 제한해야 한다.
Forward Shading과 Deferred Shading?
ㄴ 디퍼드 셰이딩은 고퀄리티 리플렉션, 다수의 다이내믹 라이트, 릿 데칼(lit decal), 그리고 모바일용 디폴트 포워드 렌더링 모드에서는
대체로 사용할 수 없는 다른 고급 라이팅 기능을 지원한다.
셰이딩 렌더러를 사용한 VR 개발하기
ㄴ Forward Rendering 설정에서 활성화 (언리얼 엔진은 기본적으로 디퍼드 렌더러를 사용)
ㄴ 안티 앨리어싱 MSAA로 변경
디퍼드 렌더링은 VR 소프트웨어를 실행하는데 적합하지 않다. 성능 때문에
텍스처 아틀라스(= 스프라이트 시트)를 이용해 최적화하기
ㄴ 작은 이미지를 한곳에 모아 놓은 한 장으 ㅣ이미지
ㄴ TexCoord 노드의 UTiling과 VTiling 속성으로 이미지를 줌인 줌아웃하고 Add 노드로 오프셋을 조정
3D 모델이 적용된 머터리얼을 텍스처로 굽기
ㄴ 여러 머터리얼이 적용된 스태틱 메시가 레벨에 많으면 비용이 많이 들기 때문에
하나의 물체에 하나의 머터리얼만 사용하여 한번의 드로우 콜만 일어나도록 셰이딩 결과를 텍스처로
구워서 복잡도를 낮출 수 있다.
액터 병합 툴 이용하기
ㄴ Tools > Merge Actor > Merge Materials 체크
ㄴ 텍스처 사이즈를 2048x2048로 설정하여 원본과 비슷하게 만들 수 있다.
HLOD 도구로 여러 메시 결합하기
ㄴ 레벨에 개별적으로 배치된 스태틱 물체를 가져다가 하나의 메시로 결합해 드로우 콜을 줄일 수 있게 해준다.
ㄴ World Settings의 HLOD System 카테고리에서 레벨을 추가할 수 있다.
ㄴ HLOD Outliner 툴을 사용해 레벨에 존재하는 메시들을 클러스터로 그룹화한다. 프록시 메시를 빌드할 수 있다.
ㄴ 멀어질수록 = 레벨이 높을수록 더 많은 액터들이 합쳐진다. 합쳐지는 물체들을 둘러싼 구체의 크기가 크다.
HLOD를 사용혀려면 모든 레벨에서 활성화해야 한다. (월드 세팅)
HLOD는 스태틱 메시에만 사용할 수 있다.
일반적인 머터리얼 최적화 기법
ㄴ 머터리얼 퀄리티 레벨 시스템 사용하기
ㄴ 머터리얼 Quality Switch 노드
ㄴ 장치의 기능 레벨에 따라 특정 동작을 하게 하는 머터리얼을 만들 수 있다. (낮은 사양의 그래픽
카드를 탑재한 장치에서도 동작하게 할 때 셰이더에서 덜 복잡한 수학적 연산을 사용하도록)
------------------------------------------9----------------------------------------
동일한 모델에 무작위성 추가하기
ㄴ 모델의 인스턴스에 약간의 변화를 추가하여 보이는 방식을 변경하고 싶을 때
ㄴ Instanced Static Mesh 컴포넌트에 머터리얼 할당하고 Instance 배열에 요소 추가
ㄴ 머터리얼에 Per Instance Random 노드를 사용하여 인스턴스마다 다른 부동소수점 값을 사용할 수 있다..
ㄴ Camera Depth Fade 노드를 Opacity Mask에 연결 (카메라에 너무 가까운 물체를 숨김)
ㄴ Per Instanced Fade Amount 노드를 카메라에서 가까이 있는 모델 대신에 멀리 있는 모델을 숨기는 데 사용할 수 있다.
(카메라에서 인스턴스의 자체 위치에 따라서 값을 제공)
+ HISM (Hierarchical Instanced Static Mesh) 컴포넌트도 비슷한 상황에서 성능을 향상시키는데 사용되는 컴포넌트이다.
일반 스태틱 메시에 다른 레벨 오브 디테일을 생성해 인스턴싱함
차폐된 영역에 먼지 추가하기
ㄴ 조명 빌드가 선행되어야 한다 (라이트 맵이 필요) (레벨에 배치된 조명이 Stationary나 Static으로 설정되어 있어야 한다.)
ㄴ 월드 세팅 > Lightmass > Generate Ambient Occlusion 속성 켜기
ㄴ PrecomputedAOMask 노드 : 머터리얼에서 조명데이터를 이용하여 질감을 계산하도록 돕는 노드
+ 무버블 조명을 사용할 때는 다른 기법을 사용할 수 있다. 외부 소프트웨어에서 AO 텍스처를 생성하거나 Distance To Nearest Surface 노드와
같은 유사한 노드를 사용할 수 있다.
여러 메시를 가로지르는 텍스처 좌표 일치시키기
ㄴ 하나의 텍스처가 여러개의 메시에 자연스럽게 이어지며 입혀지도록 하는 방법
ㄴ 물체의 UV 좌표 대신 월드 좌표계에서의 각 픽셀 위치를 활용하여 텍스처를 입힌다.
ㄴ Absolute world Position노드와 PixelNormalWS 노드를 활용한다.
퀄리티 스위치를 통한 머터리얼 복잡도 조절하기 (UE4)
ㄴ Quality Swith 노드, Feature Level Switch 노드 (ue5는 메인 머터리얼 노드에 테셀레이션 멀티플라이어 입력이 없다..)
UE4에서 UE5로 넘어가면서 테셀레이션 관련 설정은 나나이트로 대체되었다. 메인 머터리얼 노드의 World Displacement 속성과
Tesselation Multiplier 속성이 없다.
+ 모바일 셰이더 모델 : ES2 ES3_1
PC : SM5 SM6
인테리어 큐브맵 사용해 건물 내부 텍스처링하기 (건물 내부를 실제적으로 보이게 하는 기법)
ㄴ Interior Cubemap 노드, 마스크 텍스처 사용하여 마스크된 영역에 큐브맵을 적용
ㄴ 큐브맵 생성은 큐브맵 도구를 사용해서 만들어야 한다.
https://docs.unrealengine.com/4.26/ko/RenderingAndGraphics/Textures/Cubemaps/CubemapTools/
완전 절차적 Noise 패턴 사용하기
ㄴ 비용이 상당하기 때문에 다른 애셋을 생성하는 수단으로 이 기법을 사용하는 것이 좋다.
ㄴ 다양한, 반복패턴이 없는 텍스처 제작에 사용
ㄴ Noise 노드가 사용됨
ㄴ Scale 속성 : (커질수록 노이즈가 작아짐)
ㄴ Quality 속성 : (높을수록 전이가 일어나는 부분이 부드러워짐)
ㄴ Function 속성 : 패턴을 생성하는 로직을 제어
+ Function은 종류에 따라 명령어의 개수가 다르고, 특정 효과를 표현하는데 특화된 경우가 많아 신경 써서 알아두면 좋다.
읽은 책
- 언리얼 엔진 머터리얼 (나중에 정리함) 2022.12.12 1
- 3. 소켓 프로그래밍 2022.09.10
- 2. 컴퓨터 네트워크 2022.09.09 2
- 1. 멀티 스레딩 2022.09.08
- 15. 런타임 게임플레이 기반 시스템 2022.08.20
- 14. 게임플레이 시스템의 소개 2022.08.17
- 12. 충돌과 강체 역학 2022.08.17
- 11. 애니메이션 시스템 2022.08.13
언리얼 엔진 머터리얼 (나중에 정리함)
3. 소켓 프로그래밍
온라인 게임 프로그래밍에서 소켓은 파일 핸들 방식과는 다르다.
1. 클라이언트 개수만큼 소켓이 있어야 한다.
2. 파일 처리를 하는동안 스레드가 대기하는 일이 없어야 한다.
이러한 이유 때문에 소켓은 보통 비동기 입출력 상태로 다룬다. 소켓을 비동기 입출력으로 다루는 방식에는 크게 두 가지 방식이 있다.
1. 논블로킹 소켓
2. Overlapped I/O 방식
발전된 버전
3. epoll
4. I/O Completion Port (IOCP)
3.1 블로킹 소켓
스레드에서 네트워크 수신을 하는 함수를 호출하면, 수신할 수 있는 데이터가 생길 때까지 스레드는 waitable 상태 (블로킹) 가 된다.
3.3 블로킹과 소켓 버퍼
소켓 각각은 송신 버퍼와 수신 버퍼를 하나씩 가지고 있다.
TCP 송신
소켓.send(data) 함수는 블로킹 없이 바로 리턴된다. (송신 버퍼가 가득 찬 경우가 아니라면)
TCP 수신
수신 버퍼가 가득 차면 사용자는 소켓에서 데이터를 수신하는 함수를 호출하여 수신된 데이터를 꺼낼 수 있다.
수신 버퍼가 완전히 비어 있으면 블로킹이 일어난다.
3.5 수신 버퍼가 가득 차면 발생하는 현상
TCP 수신 함수인 recv()는 1바이트라도 수신할 수 있으면 즉시 리턴한다.
수신 함수가 수신 버퍼에서 데이터를 꺼내는 속도가 운영체제가 수신 버퍼의 데이터를 채우는 속도보다 느리면 수신 버퍼가 꽉 차게 된다.
ㄴ 보내는 쪽의 송신 함수 send()가 블로킹된다. TCP 연결이 끊어지는 것은 아니다.
ㄴ 실제 송신 속도는 데이터를 느리게 처리하는 쪽에 맞추어진다.
ㄴ TCP는 초당 보내는 데이터양이 수신자가 초당 수신할 수 있는 데이터양보다 많을 때, 송신자 측 운영체제가 알아서 초당 송신량을 줄인다. (흐름 제어)
UDP의 경우
받는 쪽에서의 수신 함수는 수신 버퍼에 데이터그램이 하나라도 있으면 즉시 리턴한다.
수신 버퍼가 가득 차도 송신 함수에서 블로킹이 발생하지 않는다. (데이터그램은 버려진다.)
ㄴ 수신버퍼의 처리 속도가 느리면 데이터그램 유실이 발생한다.
ㄴ UDP에는 흐름 제어 기능이 없기 때문에 UDP를 속도 제한 없이 마구 송신하면 주변의 네트워킹이 경쟁이 밀려 혼잡 현상이 발생할 수 있다.
3.6 논블록 소켓
네트워킹을 해야하는 대상이 여럿인 경우에는 네트워킹 개수만큼 스레드를 만들고 각 스레드는 각각의 네트워킹 대상과 데이터를 주고받도록 하면 된다.
ㄴ 네트워크 대상이 많지 않을 때는 문제가 없다.
ㄴ 스레드 개수가 수백 개, 수천 개인 경우에는 컨텍스트 스위치 문제 때문에 자원 낭비로 이어진다.
논블록 소켓
ㄴ 송신 함수, 수신 함수에서 블로킹이 발생하지 않는다. (즉시 리턴한다, 리턴 값은 성공, 혹은 would block 둘 중에 하나)
ㄴ 대부분 운영체제에서 소켓 함수가 블로킹되지 않게 하는 API를 추가로 제공한다.
ㄴ 한 스레드에서 여러 소켓을 한꺼번에 다룰 수 있다.
ㄴ 플랫폼마다 다른 오류 처리 코드를 반환하기 때문에 잘 처리해야 한다.
논블록 소켓으로 연결을 시도하는 경우
ㄴ would block을 리턴한 경우 would block이 끝났는지 확인하기 위한 방법을 사용해야 한다.
ㄴ 클라이언트 입장에서는 게임 루프마다 길이가 0바이트인 데이터를 송신함수로 보내 성공 값을 받으면 연결이 성공한 것으로 간주하면 된다.
ㄴ 서버 입장에서는 스핀 락 방식으로 구현할 수 없다. poll 함수를 활용해야 한다. (CPU를 최대한 아껴야 하기 떄문에)
Poll 함수 (select 함수)
ㄴ 소켓 리스트와 블록 시간을 입력값으로 받는 함수
ㄴ 리스트 내 소켓중 하나라도 I/O 처리를 할 수 있게 되면 바로 리턴, 아니라면 지정된 타임아웃 시간 이후 리턴
논블록 accept
ㄴ 블로킹 모드인 경우 리스닝 소켓에 대해 accept()를 호출하면 accept()는 블로킹이 걸린다. 그리고 TCP 연결이 들어오면 리턴을 하는데, 이때 accept()의 리턴 값은 새 TCP 연결에 대한 소켓 핸들이다.
ㄴ 논블록 소켓을 사용한 I/O 함수는 연결이 안되었으면 블로킹 대신 would block 오류 코드를 주기 때문에 반복해서 재시도 호출을 해야 한다. Poll 함수를 사용하여 I/O 가능 이벤트가 감지될 때 accept()를 호출하도록 하여 CPU 사용량 폭주 문제를 해결해야 한다.
3.7 Overlapped I/O 혹은 비동기 I/O
논블록 소켓의 장점
ㄴ 스레드 블로킹이 없으므로 중도 취소 같은 통제가 가능하다.
ㄴ 스레드 개수가 1개여도 여러 소켓을 다룰 수 있다. (호출 스택 메모리 비용, 컨텍스트 스위치 비용이 작다)
논블록 소켓의 단점
ㄴ 소켓 I/O 함수가 리턴한 코드가 would block인 경우 재시도 호출 낭비가 발생한다.
ㄴ 소켓 I/O 함수를 호출할 때 입력하는 데이터 블록에 대한 복사 연산이 발생한다.
ㄴ connect() 함수는 재시도 호출을 하지 않지만, send() 함수나 receive() 함수는 재시도 호출해야 하는 API가 일관되지 않는다는 문제가 있다.
재시도 호출 낭비의 예시
UDP의 send()는 송신 버퍼가 1바이트라도 비어 있으면 I/O 가능인데 UDP는 데이터의 일부만 보낼 수 없으므로 넣을 수 있는 데이터 크기만큼의 공간이 비어있지 않다면 would block이 발생한다. I/O 가능인데 would block이므로 CPU로 낭비로 이어진다.
소켓 함수 내부의 데이터 복사 부하의 예시
ㄴ CPU 안에 있는 캐시 메모리에 내용이 복사되어 있으면 데이터 액세스는 매우 빠르지만, 캐시에 없는 데이터를 액세스할 때는 RAM을 액세스하는데 이것은 느리다.
Overlapped I/O는 이 문제들을 해결할 수 있다.
ㄴ Overlapped I/O 함수는 즉시 리턴되지만, 운영체제로 해당 I/O 실행이 별도로 동시간대에 진행되는 상태이기 때문에 I/O 함수가 비동기로 하는 일이 완료될 때까지 소켓 API에 인자로 넘긴 데이터 블록을 제거하거나 내용을 변경해서는 안된다.
ㄴ I/O 함수의 인자로 보내는 Overlapped status 구조체 또한 중간에 내용을 건드려서는 안된다.
Overlapped I/O 전용 송신 함수를 호출하면 운영체제는 송신할 데이터가 저장되어 있는 메모리 블록 자체를 송신 버퍼로 사용한다. 수신할 때는 수신 데이터 블록 자체를 수신 버퍼로 사용한다.
논블록 방식 대비 Overlapped I/O의 장단점
장점
소켓 I/O 함수 호출 후 would block 값인 경우 재시도 호출 낭비가 없다.
소켓 I/O 함수를 호출할 때 입력하는 데이터 블록에 대한 복사 연산을 없앨 수 있다.
send, receive, connect, accept 함수를 한 번 호출하면 이에 대한 완료 신호는 딱 한 번만 오기 때문에 프로그밍 결과물이 깔끔하다.
I/O completion port와 조합하면 최고 성능의 서버를 개발할 수 있다.
윈도우 플랫폼에서만 사용할 수 있다.
단점
완료되기 전까지 Overlapped status 객체나 데이터 블록을 중간에 훼손하지 말아야 한다.
accept, connect 함수 계열의 초기화가 복잡하다.
논블록 소켓은 리액터 패턴, Overlapped I/O는 프로액터 패턴이다.
3.8 epoll (논블록 방식)
epoll은 안드로이드와 리눅스에서만 사용 가능하다.
IOS, MacOs, FreeBSD에서는 epoll과 매우 유사한 kqueue를 사용하면 된다.
epoll에 논블록으로 설정된 소켓을
select를 쓰면 모든 소켓에서 루프를 돌지만 epoll을 사용하면 I/O 가능인 상태의 소켓에서만 루프를 돌면 된다.
소켓의 송신 버퍼가 빈 공간이 없는 순간을 유지하는 순간은 짧기 때문에 대부분의 소켓은 I/O 가능 상태이다. (비효율적이다)
이 문제를 해결하기 위해 레벨 트리거 대신 에지 트리거를 써야 한다.
레벨 트리거 : I/O 가능이면 epoll에서 항상 꺼내어진다.
에지 트리거 : I/O 가능이 아니었다가 가능으로 변하는 순간에만 epoll에서 꺼내어진다.
에지 트리거를 쓸 때 주의해야 할 점
1. I/O 호출을 한 번만 하지 말고 would block이 발생할 때까지 반복해야 한다.
2. 소켓은 논블록으로 미리 설정되어 있어야 한다.
3.9 IOCP
epoll은 논블록 소켓을 대량으로 갖고 있을 때 효율적으로 처리해 주는 API이다.
IOCP는 Overlapped I/O를 다루는 운영체제에서 사용하는 API이다.
IOCP는 소켓의 Overlapped I/O가 완료되면 이를 감지해서 사용자에게 알려 주는 역할을 한다.
ㄴ epoll과 비슷하지만 epoll은 I/O 가능인 것을 알려 주지만, IOCP는 I/O 완료인 것을 알려 준다.
epoll과 비교했을 때 사용법에 복잡한 기능이 몇 가지 있는데, 그중 하나가 Accept 처리이다.
1. IOCP에 listen socket L을 추가했다면, L에서 TCP 연결을 받을 경우 이에 대한 완료 신호가 IOCP에 추가됩니다.
2. 단 사전에 이미 AcceptEx로 Overlapped I/O를 건 상태여야 한다.
3. IOCP로 L에 대한 이벤트를 얻어 왔지만, 앞서 Overlapped accept처럼 SO_UPDATE_ACCEPT_CONTEXT와 관련된 처리를 해 주어야 새 TCP 소켓 핸들을 얻어 올 수 있다.
IOCP는 epoll에서 할 수 없는 성능상 유리한 기능이 있다. (스레드 풀링을 활용하여 여러 가지 일을 스레드 몇 개가 골고루 분담해서 처리할 수 있다)
'읽은 책 > 게임 서버 프로그래밍 교과서' 카테고리의 다른 글
2. 컴퓨터 네트워크 (2) | 2022.09.09 |
---|---|
1. 멀티 스레딩 (0) | 2022.09.08 |
2. 컴퓨터 네트워크
2.3 컴퓨터 네트워크 데이터
OSI 모델의 2계층에서는 데이터 단위를 프레임이라 하고, 3계층에서는 패킷이라고 한다.
프로그래머는 이것들 대신 스트림과 메세지라는 것을 주로 다룬다.
2.3.1 스트림 형식
스트림 : 두 단말기를 연결한 후 그 연결을 끊기 전까지 한쪽에서 다른 한쪽으로 연결된 데이터 흐름
컴퓨터 네트워크에서 스트림의 개념은 파일의 스트림과 다르다.
ㄴ 데이터가 여러 부분으로
ㄴ 스트림 송신 횟수와 수신 횟수가 불일치할 수 있다.
데이터가 여러 부분으로 나뉘어질 수 있다면 사용자가 어떻게 받을지 정의를 해주어야 한다.
ㄴ 데이터의 크기를 헤더로 붙이는 방식
ㄴ 구분자를 이용하는 방식
2.3.2 메세지 형식
보낸 데이터의 수와 받는 데이터의 수가 같다. (각 데이터가 정확히 구별된다)
운영체제에 내장된 네트워크 모듈인 네트워크 스택에서 프로그램이 보내고 받는 스트림과 메세지를 관리해주기 때문에
IP패킷의 크기가 제한적이더라도 스트림이나 메세지에서는 크기 제한이 없다.
프로그램이 매우 긴 스트림을 송신할 때 운영체제는 이를 IP 패킷의 크기 제한에 맞추어 여러 조각을 낸다 (단편화)
각 조각은 IP 패킷 하나하나가 되어 받는 쪽에 송신한다.
받는 쪽에서의 복원 과정도 운영체제 안에서 진행된다.
2.4 컴퓨터 네트워크 식별자
IPv4는 4바이트
IPv6는 16바이트
포트는 2바이트
2.5 컴퓨터 네트워크의 품질과 특성
2.5.1 네트워크의 품질을 저해하는 것들
1. 스위치나 라우터가 자신이 처리할 수 있는 한계를 넘는 데이터를 수신할 경우
ㄴ 자기가 처리할 수 있는 것 이상을 그냥 버리기
ㄴ 아직 처리하지 못한 것들을 메모리에 누적하기
2. 데이터가 오가는 선로에 문제가 있는 경우
ㄴ 회선 신호가 약하거나 잡음이 섞이면 패킷 유실이 발생할 수 있다.
2.5.2 전송 속도와 전송 지연 시간
전송 속도 : 두 기기 간에 초당 전송될 수 있는 최대 데이터 양, 보통 bps로 표현 (= 두 단말기 사이에 있는 네트워크 기기 중 최소 스루풋에 의해 결정)
레이턴시 : 두 기기 간에 최소량의 데이터를 전송할 때 걸리는 시간 (= 두 단말기 사이에 있는 네트워크 기기의 레이턴시 총합)
전송 속도에 영향을 주는 것
ㄴ 선로의 종류와 품질
ㄴ 두 기기의 소프트웨어와 하드웨어 종류
레이턴시에 영향을 주는 것
ㄴ 선로의 종류와 품질
ㄴ 송신자-수신자 사이의 라우터 처리 속도
송신자와 수신자 사이에는 스위치, 라우터, 방화벽 같은 네트워크 기기들이 있고 각각의 기기들은 수신한 데이터를 처리 가능한 데이터로 변환하고 그것에 대한 연산 처리를 하기 때문에 시간을 차지한다.
2.5.3 네트워크 품질 기준 세 가지
전송 속도, 패킷 유실률, 레이턴시
2.5.4 무선 네트워크의 품질
다른 기기들이 동시에 신호를 보내면 신호가 변조되기 때문에 다른 전파가 감지되는지 확인 후 데이터를 보내기 때문에 (CSMA) 레이턴시가 길게 나온다.
2.6 컴퓨터 네트워크에서 데이터 보내기와 받기
2.6.1 UDP 네트워킹 (User Datagram Protocol)
받는 쪽의 프로그램과 보내는 쪽의 프로그램이 각자 데이터 통신에 사용할 포트를 할당하고 있어야 한다.
UDP를 이용하여 데이터그램을 보낼 수 있는데, 데이터그램은 64KB 이하의 이진 테이터로, 스트림이 아닌 메세지 성질을 가진다.
ㄴ 데이터그램 내용이 훼손되지는 않지만 패킷 유실이 발생할 수 있다. (데이터그램을 못 받거나 같은 데이터그램을 두 번 이상 받을 가능성이 있다)
ㄴ 받는 쪽에서 데이터그램 유실이나 순서 뒤바뀜 혹은 중복 수신 현상이 발생해도 괜찮은 경우에만 UDP를 사용하는 것이 좋다. (예를 들면 동영상이나 음성 데이터, 네트워크 게임에서는 캐릭터의 이동 정보)
UDP로 데이터를 주고받으려면 소켓을 생성하고 포트를 바인드 한뒤 송신, 수신 작업을 진행하면 된다.
UDP의 특징
수신용 소켓과 송신용 소켓을 따로 만들지 않아도 된다.
다대다 통신이 가능하다. (하나의 소켓이 여러개의 소켓으로 송수신 할 수 있다)
2.6.2 TCP 네트워킹
데이터를 주고받기 전에 연결을 해줘야 한다.
TCP는 메세지 형태가 아니라 스트림 형태로 데이터들을 전달한다.
UDP는 IP 패킷 유실이 발생하면 UDP 데이터그램도 드롭되지만 TCP는 똑같은 상황에서도 흐름 제어 기능이 있기 때문에 데이터가 상대방에게 정확하게 전송된다.
TCP에서 보낼 스트림 데이터는 세그먼트라는(IP 패킷안에 넣을 수 있는 크기) 단위로 쪼개져 수신자에게 전송된다.
수신자가 IP 패킷을 받으면 ack를 회신하는데 보낸 쪽에서는 일정 시간 안에 ack이 회신되지 않으면 응답이 올때까지 다시 세그먼트를 보낸다.
TCP 소켓 사용법
보내는 쪽
1. TCP 소켓 생성
2. 포트 바인딩
3. 연결 (받는 쪽 ip:port) (블로킹)
4. 송신 (이미 연결 되어있으므로 ip:port가 아닌 데이터만 사용)
5. 필요한 만큼 송신했다면 연결을 끊는다. (close)
받는 쪽
1. TCP 소켓 생성
2. listen (포트)
3. accept() (연결 될때 까지 블로킹, 새로운 소켓 생성)
4. 반복문내에서 recv()로 데이터를 받는다. 연결 상태를 유지하다가 0바이트 크기의 데이터를 받으면 반복문을 탈출하고 연결을 끊는다. (close)
ㄴ UDP에서는 데이터그램을 메세지 형식으로 주고받는다. 0바이트 메세지도 허용한다. TCP는 크기 0의 데이터를 받으면 연결이 종료되었음을 의미한다.
TCP 소켓도 UDP 소켓과 마찬가지로 소켓 하나로 송수신을 모두 할 수 있다.
2.7 패킷 유실 시 UDP와 TCP에서 현상
UDP는 1만 바이트의 데이터그램을 UDP로 전송하면 운영체제는 이를 1300바이트 정도의 크기 8개로 나누어 IP 패킷 8개로 전송되는데 이들 중 하나라도 유실되면 수신자는 8개 모두 못 받는다. (1만바이트짜리 데이터그램이 유실된다)
TCP에서는 유실되는 패킷의 재전송 시간만큼 지연 시간이 발생한다.
UDP의 레이턴시 = 네트워크 기기의 레이턴시
TCP의 레이턴시 = 네트워크 기기의 레이턴시 + 패킷 유실률 * 재전송 대기 시간
네트워크 게임 개발에서 거의 모든 메세지 종류에는 TCP를 사용하고 특정 상황에서만 UDP를 사용하지만 실제 통신량의 대부분은 UDP가 차지한다.
2.8 주로 사용하는 메세지 형식
1. 텍스트 형식
ㄴ HTTP나 JSON 같은 표준화된 형식을 씀
2. 바이너리 형식
ㄴ 각 바이트 혹은 비트 필드는 특정한 의미를 가진다.
ㄴ 텍스트 형식에 비해 구문 분석기가 필요 없어서 처리 성능이 더 낫고, 통신량도 적다
메세지 안에 내용을 담는 것뿐만 아니라 그 메세지 내용이 어떤 것을 담고 있는지의 정보를 같이 포함시키는 형식
ㄴ 메타 데이터를 포함시키면 데이터 통신량이 많아지고, 해커에게도 더 쉽게 노출된다는 단점이 있지만 하위 호환성 측면에서 유리하다.
ㄴ 받는쪽과 보내는쪽의 버전이 달라도 호환성 문제를 해결할 수 있다.
2.9 네트워크 주소 변환
네트워크 주소 변환 (NAT : network address translation)
ㄴ 다른 단말기로 전송되던 패킷의 송신자 주소나 수신자 주소가 다른 것으로 변환되는 과정
ㄴ 인터넷 공유기는 NAT 라우터이다.
공유기 안에 있는 기기가 공유기 밖에 있는 기기에 패킷을 보내려고 하면 패킷을 통과하여 수신지에 전달
공유기는 내부 주소와 외부 주소의 매핑 정보를 생성해서 가지고 있는다.
2.11 더 읽을 거리
TCP 흐름 제어 (ARQ)
ㄴ slicing window (윈도우 크기만큼의 패킷을 응답받지 않고 보내기)
ㄴ 송신측의 처리 속도가 수신측의 처리 속도보다 빨라서 수신측의 저장용량을 초과한 데이터가 손실되는 것을 방지하기 위함
TCP 혼잡 제어
ㄴ AIMD (additive increase, multiplicative decrease), slow start, fast retransmit, fast recovery
네트워크 게임의 성능 최적화를 위해 UDP 위에 흐름 제어를 별도로 구현하는 경우도 있다.
송신자와 수신자 간 오가는 메세지는 제 3자가 도청하거나 변조할 수 있기 때문에 메세지를 암호화해야 한다.
'읽은 책 > 게임 서버 프로그래밍 교과서' 카테고리의 다른 글
3. 소켓 프로그래밍 (0) | 2022.09.10 |
---|---|
1. 멀티 스레딩 (0) | 2022.09.08 |
1. 멀티 스레딩
1.4 스레드 정체
CPU 개수와 Runnable 스레드 개수가 같거나 Runnable 스레드 개수가 더 적으면 컨텍스트 스위치가 발생할 이유가 없다. 하지만 Runnable 스레드 개수가 더 많으면 컨텍스트 스위치가 어느 CPU 안에서는 반드시 발생한다. (비둘기집 원리)
멀티스레딩을 사용하여 소수 구하기
1 ~ 150000 사이의 소수들을 찾는 프로그램
1.8 잠금 순서의 규칙
여러 뮤텍스를 사용할 때 교착 상태를 예방하려면 각 뮤텍스의 잠금 순서를 먼저 그래프로 그려 두고, 잠금을 할 때 잠금 순서 그래프를 보면서 거꾸로 잠근 것이 없는지 체크해야 한다.
재귀 뮤텍스
ㄴ 한 스레드가 같은 뮤텍스를 여러 번 반복해서 잠궈도 된다. 물론 잠금 횟수만큼 풀어야 한다.(재귀 잠금이 가능하다.)
ㄴ 재귀 잠금은 잠금 순서 그래프에 상관 없이 교착 상태를 일으키지 않는다. (첫 잠금에서만 순서를 지키면 된다)
1.9 병렬성과 시리얼 병목
여러 CPU가 각 스레드의 연산을 싫애하여 동시 처리량을 올리는 것을 병렬성이라고 한다. 병렬로 실행되게 프로그램을 만들었는데 정작 한 CPU만 연산을 수행하는 현상을 시리얼 병목이라고 한다.
시리얼 병목이 있을 때 CPU 개수가 많을수록 총 처리 효율성이 떨어지는 현상을 암달의 법칙(Amdahl's Law)라고 한다.
Visual Studio의 Concurrency Visualizer도구
ㄴ 멀티 스레드 프로그램이 병렬적으로 여러가지 일들을 동시에 잘 수행하는지 분석하여 시각화해서 보여주는 도구
https://marketplace.visualstudio.com/itemsitemName=Diagnostics.DiagnosticsConcurrencyVisualizer2019#overview
1.10 싱글 스레드 게임 서버
1.11 멀티 스레드 게임 서버
1.12 스레드 풀링
처리할 이벤트를 큐에 쌓아두고 비어 있는 스레드에 이벤트를 처리하도록 배분하는 방식이다.
스레드 개수를 클라이언트 개수만큼 두지 않고 스레드 풀링을 사용한다.
CPU 코어가 8개 있는 8개의 스레드를 실행시키는 상황에서 스레드가 CPU 연산만 하는 것이 아니라 데이터베이스나 파일 등에 액세스하는 스레드여서 CPU 사용율이 25%이고 디바이스 타임이 75%인 상황이라면 스레드 풀의스레드의 갯수를 4배 늘려 CPU를 놀게 하지 않게 해야 한다.
CPU 연산이 주 역할인 서버의 스레드 풀의 스레드 개수는 서버의 CPU 코어 개수와 동일하게 잡아도 충분하다.
1.13 이벤트
잠자는 스레드를 깨우는 도구로써 사용된다.
event는 내부적으로 0과 1의 상태 값을 가진다. (이벤트가 없으면 0 있으면 1)
윈도우 API에 이벤트 관련 함수가 있다.
https://docs.microsoft.com/ko-kr/windows/win32/sync/using-event-objects
자동 이벤트
ㄴ 이벤트 상태 값을 0으로 갖고 있다가 신호를 받으면(SetEvent) 상태 값이 1로 바뀌는데 이때 이벤트를 기다리던(잠들어 있던) 스레드 (Wait)가 있다면 그 스레드를 깨우고 자동으로 0의 상태 값으로 바뀐다.
ㄴ 두개 이상의 스레드를 이벤트를 기다리고 있었다면 여러 스레드중 하나만 깨어난다.
수동 이벤트
ㄴ 이벤트가 신호를 받으면 대기하고 있던 모든 스레드가 깨어난다.
ㄴ SetEvent()를 한 스레드에서 호출하면 대기하던 스레드에서 SetEvent(0)을 호출하여 수동으로 이벤트 상태 값을 되돌려줘야 한다. 하지만 이 방법을 사용하면 대기하던 스레드중 일부만 깨어날 가능성이 있다.
ㄴ PulseEvent()를 사용하여 이벤트에 신호를 주면 모든 스레드를 깨우면서 상태값을 자동으로 0으로 되돌려 준다.
1.14 세마포어
뮤텍스는 1개의 스레드만 자원을 액세스할 수 있게 하지만 세마포어는 원하는 개수의 스레드가 자원을 액세스할 수 있게 한다.
Wait() ~ Release() (mutex의 lock unlock같은 느낌)을 사용한다.
윈도우 API에 세마포어 관련 함수가 있다.
https://docs.microsoft.com/ko-kr/windows/win32/sync/using-semaphore-objects
스레드 간에 공유되는 큐가 있고 한 스레드는 큐에 항목을 넣고 다른 한 스레드는 큐에서 항목을 꺼내 쓰는 디자인에 활용하기 좋다.
초기값이 0인 세마포어를 만든 뒤, 항목을 넣는 스레드에서는 세마포어.Release()를 하면서 큐에 푸시하고 꺼내는 스레드에서는 Wait()를 하여 세마포어 값이 1 이상인 경우에만 큐에서 하나씩 요소를 꺼낸다.
1.15 원자 조작
원자 조작은 뮤텍스나 임계 영역 잠금 없이도 여러 스레드가 안전하게 변수에 접근할 수 있게 해준다.
32비트나 64비트 변수 타입에 여러 스레드가 접근할 때 한 스레드씩만 처리됨을 보장한다.
mutex보다 속도가 더 빠르다.
https://lemonyun.tistory.com/103
'읽은 책 > 게임 서버 프로그래밍 교과서' 카테고리의 다른 글
3. 소켓 프로그래밍 (0) | 2022.09.10 |
---|---|
2. 컴퓨터 네트워크 (2) | 2022.09.09 |
15. 런타임 게임플레이 기반 시스템
15.1 게임플레이 기반 시스템의 컴포넌트
대부분의 게임 엔진은 게임의 고유한 규칙과 목표, 그리고 동적 월드 구성 요소들을 만드는 기반으로 쓰이는 여러 런타임 소프트웨어 컴포넌트를 제공한다. 이런 컴포넌트를 게임플레이 기반 시스템이라고 부르기로 하자.
게임 엔진마다 게임 플레이 소프트웨어 디자인에 대한 접근이 조금씩 다르다.
대부분의 엔진이 지원하는 주요 하위 시스템
1. 런타임 게임 객체 모델
ㄴ 게임디자이너가 월드에디터를 통해 인지하는 가상의 게임 객체 모델을 실제로 구현하는 부분
2. 레벨 관리 및 스트리밍
ㄴ 게임 플레이가 벌어지는 가상 월드의 콘텐츠를 불러오고 내리는 시스템,
ㄴ 레벨 데이터를 메모리에 스트리밍하면 플레이어는 광활하고 연속된 월드에 있는 것처럼 인지하게 된다.
3. 실시간 객체 모델 업데이트
ㄴ 게임 객체들이 스스로 동작하게 하려면 모든 객체를 주기적으로 업데이트해줘야 한다.
4. 메세지와 이벤트 처리
ㄴ 대부분의 게임 객체들은 서로 통신해야 한다. 이런 통신은 대개 추상적 메세지 시스템을 통해 이뤄진다.
5. 스크립트
ㄴ 하이레벨 게임 로직을 C나 C++ 등의 프로그래밍 언어로 짜면 매번 컴파일을 해줘야 한다. (느리다)
생산성을 향상시키면서 빠른 반복 생산을 가능하게 하기 위해 게임 엔진에 스크립트 언어를 통합하는 경우가 많다.
파이썬이나 루아같이 텍스트 기반인 언어도 있고, 언리얼 엔진의 Kismet처럼 그래픽 언어인 경우도 있다.
6. 목적 및 게임 흐름
엔진마다 부분적으로 지원하는 시스템
1. 동적으로 게임 객체를 생성하고 파괴하기
ㄴ 게임월드의 동적인 구성 요소들을 게임 플레이 도중에 만들었다가 없애는 경우가 자주 있다.
2. 로우레벨 엔진 시스템과의 연동
ㄴ 모든 게임 객체는 적어도 한두 가지 이상의 하위 엔진 시스템과 연관이 있다. 게임 객체들이 각자 필요한 하위 엔진 시스템에 접근할 수 있게 해주는 것이 게임플레이 기반 시스템의 주요한 역할 중 하나다.
3. 새로운 게임 객체 타입을 정의할 수 있는 기능
ㄴ 새로운 객체 타입을 쉽게 추가하고 월드에디터에서 사용할 수 있게 유연한 게임 객체 모델을 갖는것이 중요하다.
ㄴ 아주 이상적인 상황이라면 데이터 주도 방식(스크립트)만으로 새로운 객체 타입을 정의할 수도 있지만 대부분의 엔진에서는 프로그래머의 손을 거친다.
4. 고유 객체 식별자(id)
ㄴ 게임 월드 안에는 다양한 종류의 게임 객체들이 수백 혹은 수천 개 존재하는 경우가 흔하기 때문에 런타임에 각 객체들을 구분하고 찾을 수 있는 기능이 중요하다.
ㄴ정수 식별자나 문자열 식별자를 사용한다.
5. 게임 객체 질의
ㄴ 게임 월드 내에서 게임 객체를 찾을 수 있는 방법이 반드시 있어야 한다.
ㄴ 예를 들면 플레이어 캐릭터 주위 20미터 안에 있는 모든 적을 찾는 것
6. 게임 객체에 대한 참조
ㄴ 객체를 찾았으면 객체를 참조하는 값을 담을 수 있어야한다.
ㄴ C++ 클래스 인스턴스에 대한 포인터나 스마트 포인터일 수도 있다.
7. 유한 상태 기계에 대한 지원
ㄴ 게임 객체 타입 중 유한 상태 기계로 모델링하면 가장 잘 들어맞는 것들이 종종 있다. 게임 객체가 한 번에 한 가지 상태를 가질 수 있게 지원하는 엔진도 있다.
8. 네트워크 레플리케이션
ㄴ멀티플레이어 게임에서의 게임 객체의 상태는 다른 모든 기계에 복제돼서 모든 플레이어들이 그 객체를 일관된 모습을 볼 수 있어야 한다.
9. 게임을 저장하고 불러오는 기능/객체의 영속성
ㄴ 게임 엔진이 월드 내의 게임 객체들의 현재 상태를 디스크에 저장하고 나중에 불러올 수 있도록 하는 기능
ㄴ RTTI, 리플렉션, 추상 생성같은 프로그래밍 언어의 기능을 필요로 한다.
RTTI, 리플렉션은 런타임에 객체의 타입 뿐만 아니라 그 객체의 클래스가 어떤 속성과 메서드를 지원하는지를 알 수 있게 해주고, 추상 생성은 클래스 이름을 하드코딩하지 않고도 그 클래스의 인스턴스를 사용할 수 있게 해준다.
15.2 런타임 객체 모델 구조
월드 에디터에서 디자이너들에게 제공되는 툴 측면 추상적 객체 모델을 게임에서 구현한 것을 런타임 객체 모델이라 할 수 있다.
런타임 객체 모델의 구현은 툴 측면의 추상적 객체 모델과 비슷할 수도 있고, 완전히 다를 수도 있다. 다양한 방식이 쓰이지만 대부분의 게임 엔진들은 다음의 두 가지 기본적인 구조 중 한 가지를 따른다.
1. 객체 중심적 구조
ㄴ 툴 측면의 게임 객체 하나는 런타임에서 클래스 인스턴스 한 개로 표현되거나, 아니면 서로 연관 있는 적은 숫자의 인스턴스 집합으로 표현된다.
ㄴ 객체는 자신의 속성과 행동을 가지며, 이것들은 객체의 클래스 내에 캡슐화 되어 있다.
2. 속성 중심적 구조
ㄴ 툴 측면의 게임 객체들은 각각 고유 id로만 표현된다.
ㄴ 각 게임 객체의 속성들은 속성 타입별로 데이터 테이블에 나뉘어 담겨 있으며, 접근할 때는 객체의 id를 이용한다.
15.2.1 객체 중심 구조
15.2.1.2 거대 단일 클래스 계층
하나의 공통 베이스 클래스를 거의 모든 객체들이 상속하는 경우
15.2.1.3 깊고 넓은 계층 구조의 문제점
ㄴ 클래스들을 이해하기 힘들고 유지 및 수정이 어려움
ㄴ 여러 계열의 분류 구조를 구현할 수 없는 문제
예를 들어 생물체를 유전적인 형질을 기준으로 분류하면서 동시에 색깔에 따라 분류할 수 없다.(다중 상속의 문제)
다중 상속(is-a)보다는 합성(has-a)이나 조합을 사용하는 편이 낫다.
ㄴ 버블업 효과
언리얼 엔진의 Actor 클래스는 렌더링, 물리, 월드와의 상호작용, 오디오 효과, 멀티플레이어용 네트워크 레플리케이션, 객체 생성 및 파괴, 액터 반복, 메세지 브로드캐스팅 등을 위한 데이터 멤버와 코드를 모두 가지고 있다. 이렇게 최상위 루트 클래스에 여러 기능이 버블업 하게 내버려 두면 여러 엔진 하위 시스템의 역할을 캡슐화하기 어려워진다.
15.2.1.4 합성을 통해 계층 구조를 단순하게 유지
합성 관계에서는 클래스 A가 클래스 B의 인스턴스를 직접 포함하거나 아니면 B의 인스턴스에 대한 포인터나 참조를 포함한다. A의 인스턴스가 생성될 때와 파괴될 때 B도 같이 생성되고 파괴되는 경우를 합성(composition) 이라고 한다. 두 클래스 사이에 포인터나 참조를 사용해 한 클래스가 다른 클래스의 생성과 파괴를 직접 관리하지 않게 만들 수도 있다. 이 경우는 조합(aggregation) 이라고 부른다.
컴포넌트 디자인을 활용하면 만들고자 하는 타입의 게임 객체가 꼭 필요로 하는 기능들만 넣게 선택할 수 있다.
https://lemonyun.tistory.com/78
컴포넌트 생성과 소유권
ㄴ 단순한 방법 : GameObject 클래스가 가능한 모든 컴포넌트의 포인터를 갖고, 게임 객체들은 GameObject를 상속받아 정의한다. GameObject의 생성자에서는 모든 컴포넌트에 대한 포인터를 NULL로 초기화한다(하드 코딩). 상속받은 클래스의 생성자는 각자 필요에 따라 컴포넌트를 생성하면 된다.
ㄴ GameObject의 소멸자에서 모든 컴포넌트에 대한 포인터를 지워주는 식으로 구현하면 편리하다.
15.2.1.5 제네릭 컴포넌트
ㄴ 유연한 (구현하기에는 까다로운 방법) : 루트 클래스에 제네릭연결 리스트를 두어 컴포넌트들을 관리하는 방식
ㄴ 객체에 새로운 타입의 컴포넌트를 추가할 때 게임 객체 클래스를 수정하지 않을 수 있다.
ㄴ 연결 리스트를 순회하며 각 컴포넌트에 대해 타입을 질의한다거나 차례로 이벤트를 넘겨 처리할 기회를 주는 등 다형적인 연산을 할 수 있다.
15.2.1.6 순수 컴포넌트 모델 (속성 중심 디자인)
ㄴ GameObject '허브' 클래스를 둘 필요 없이 각 컴포넌트마다 해당 게임 객체의 고유 id를 복사해 넣는 방법 (컴포넌트 = 속성 중심 구조)
순수 컴포넌트 모델에서는 팩토리 패턴을 사용하면 된다. 가상 함수가 있는 팩토리 클래스를 게임 객체 타입마다 한 개씩 만들고, 가상 함수를 오버라이드해 객체마다 원하는 컴포넌트를 생성하게 한다.
ㄴ 순수 컴포넌트 디자인의 문제는 컴포넌트 간 통신을 중계해줄 GameObject 클래스가 없다는 것이다. 컴포넌트 간 통신이나 다른 게임 객체로 메세지를 보내는 일도 까다롭다.
15.2.2 속성 중심적 구조
게임 객체가 가질 수 있는 모든 속성의 집합을 정의하고, 속성마다 테이블을 만든다. 테이블에서 속성과 게임객체 ID가 매핑된다. (관계형 데이터베이스에 가깝다)
객체는 속성에 의해서만 정의되는 것이 아니라 행동에 의해서도 정의되기 때문에 행동을 구현하는 방법은 여러가지이며, 엔진마다 다르게 구현한다.
15.2.2.1 속성 클래스를 통해 행동 구현
속성 타입은 속성 클래스로 구현할 수 있다.
각 속성 클래스는 하드코딩된 메서드(멤버 함수)를 통해 행동을 제공한다.
어떤 게임 객체의 전체적인 행동은 그 객체의 모든 속성들의 행동이 합쳐져 결정된다.
15.2.2.2 스크립트를 통해 행동 구현
속성 값들은 데이터베이스형 테이블에 담아 두고 게임 객체의 행동을 스크립트 코드를 이용해 구현하는 방법이 있다.
게임 객체는 ScriptId와 같은 특수한 속성을 가질 수 있고, 이 속성이 있을 경우 이것은 해당 객체의 행동을 처리할 스크립트 코드 블록을 나타낸다.게임 월드의 이벤트에 게임 객체가 반응하게 만들 때도 스크립트 코드를 사용한다.
15.2.2.4 속성 중심 디자인의 장단점
장점
ㄴ 실제로 사용되고 있는 속성 데이터만 저장하면 되기 때문에 메모리를 적게 쓰는 경향이 있다.
ㄴ 게임 코드를 컴파일할 필요 없이 새 속성을 쉽게 정의할 수 있다.
ㄴ 같은 타입의 데이터가 메모리에 연속적으로 저장되기 때문에 캐시 효율이 좋을 수 있다. (배열의 구조체 방식을 사용한다면)
단점
ㄴ 속성들 간에 어떤 관계를 강제하기가 어려워진다.
ㄴ 게임 객체가 가지는 속성을 한눈에 볼 수 없기 때문에 디버깅이 어렵다.
15.3 월드 덩어리 데이터 형식
월드 청크에는 정적 월드 요소와 동적 월드 요소가 같이 들어간다.
월드 덩어리 데이터 파일에는 게임 객체마다 다음과 같은 것들이 저장된다.
객체 속성들의 초기 값
ㄴ 월드 청크는 모든 게임 객체가 처음 게임 월드에 생성될 때의 상태를 정의한다. 객체의 상태 데이터는 여러 형식으로 저장할 수 있다.
객체 타입에 대한 지정
ㄴ 객체 중심 엔진에서는 문자열, 해시 문자열 ID, 혹은 기타 고유한 타입 id 등을 이용한다. 속성 중심 디자인에서는 타입을 그대로 저장하기도 하고, 아니면 객체를 이루는 속성들에 따라 암묵적으로 정의될 수도 있다.
15.3.1 바이너리 객체 이미지
게임 객체들을 디스크 파일로 저장하는 방법 중에는 런타임에 메모리에 존재하는 모든 객체의 바이너리 이미지를 파일로 저장하는 방식이 있다.
하지만 C++ 클래스 인스턴스를 바이너리 이미지로 저장하는 것은 문제가 있다.
ㄴ 포인터와 가상 테이블을 예외적으로 처리해야 한다.
ㄴ 클래스 인스턴스 내 데이터의 endian을 처리해야 한다.
이런 이유로 객체 이미지를 저장하는 형식은 게임 객체 데이터를 저장하는데 그다지 좋은 선택이 아니다.
15.3.2 게임 객체 정보의 직렬화
직렬화를 사용하면 바이너리 객체 이미지를 저장하는 것보다 여러 플랫폼에 옮기기 쉽고 구현하기가 간편하다.
C#이나 자바는 모두 객체 인스턴스를 XML 텍스트 형식으로 직렬화하는 표준 방식을 지원한다. 하지만 C++은 표준화된 직렬화 기능을 지원하지는 않는다.
XML은 파싱이 느리다는 단점이 있어서 월드 덩어리를 읽어 들이는 시간이 느려질 수 있다. 그래서 더 빠르게 파싱할 수 있고 크기도 더 작은 자체 구현 바이너리 형식을 사용하는 게임엔진도 있다.
객체를 디스크에 저장하고 불러오는 직렬화 기법을 구현하는 데는 다음과 같은 기본적인 두 가지 방식 중에서 하나가 쓰인다.
1. 부모 클래스에 SerializeOut()과 SerializeIn() 등의 이름이 붙은 가상 함수 두 개를 넣고 상속받는 클래스는 이 함수들을 구현해 자신들의 속성을 직렬화하게 한다.
2. C++ 클래스에 리플렉션 시스템을 구현한다. 그런 후 리플렉션 정보가 있는 모든 C++ 객체를 자동으로 직렬화하는 제네릭 시스템을 만든다. (범용적임)
속성 정보에 더해 직렬화 데이터 스트림은 반드시 객체의 클래스나 타입을 나타내는 이름이나 고유 id를 포함한다. 이런 클래스 id는 디스크에서 메모리로 객체를 직렬화할 때 올바른 클래스 객체를 생성하는데 쓰인다.
하지만 C++에서는 이름을 나타내는 문자열이나 id만 가지고 클래스 인스턴스를 생성할 수 없다. C++에서는 이런 문제를 우회하기 위해 클래스 팩토리를 사용한다. 클래스 팩토리를 구현하는 방법은 여러 가지가 있지만, 가장 단순한 방식은 데이터 테이블을 만들어 클래스이름/id를 가지고 그 클래스 인스턴스를 생성하는 하드 코딩된 함수나 펑터로 연결해주는 것이다. id에 해당하는 함수나 펑터를 테이블에서 찾아 이것들을 호출해 클래스 인스턴스를 생성한다.
15.3.3 스포너와 타입 스키마
바이너리 객체 이미지와 직렬화는 게임 객체 타입의 런타임 구현에 따라 좌우되기 때문에 월드 에디터가 게임 엔진의 런타임 구현에 대해 상세히 알고 있어야 한다는 단점이 있다.
게임 월드 에디터와 런타임 엔진 코드 간의 결합을 끊으려면 게임 객체에 대한 정보를 구현 중립적인 방식으로 추상화하면 된다. 월드 청크 데이터 파일에 있는 모든 객체마다 작은 데이터 블록(스포너)를 저장한다. 스포너는 가볍고 데이터만 있는 게임 객체 표현으로, 런타임에 해당 게임 객체의 인스턴스를 생성하고 초기화하는 데 사용된다.
스포너에는 툴에서 쓰이는 게임 객체 타입에 대한 id, 속성들의 초기값을 저장한 테이블, 월드 행렬 등이 저장된다.
게임 객체를 생성(스폰)할 때 스포너의 타입을 보고 올바른 클래스 인스턴스가 생성된다.
스포너는 로딩되자마자 해당하는 게임 객체를 생성하게 설정할 수도 있고 게임 실행 중 생성 요청이 올 때까지 기다릴 수도 있다.
스포너는 게임 월드 내의 중요 지점이나 좌표축들을 정의하는데 사용될 수도 있다.
15.3.3.1 객체 타입 스키마 (= 게임 객체 스키마)
스포너 방식의 디자인을 채용한 게임 월드 에디터에서는 게임 객체 타입을 나타낼 때 데이터 기반 방식의 스키마를 사용할 수 있는데, 스키마는 어떤 타입의 객체를 만들고 편집할 때 알아야 할 속성들을 정의한 것이다.
객체 타입 스키마를 클래스처럼 상속받는 형태로 지원하는 엔진도 있다. 객체 타입을 비롯해 런타임에 다른 게임 객체와 구분할 수 있는 고유 id는 모든 게임 객체에 있어야 하는데, 이것들은 탑 레벨의 스키마에 정의해서 다른 스키마들이 상속 받을 수 있게 한다.
15.3.3.2 디폴트 속성 값
게임 디자이너가 어떤 게임 객체 타입의 인스턴스를 게임 월드에 집어넣고자 할 때 수많은 속성의 값을 정해줘야 하기 때문에 스키마들의 속성들 중에 디폴트 값을 정의할 수 있는 것들이 많다면 큰 도움이 된다.
15.4 게임 월드의 로딩과 스트리밍
오프라인 월드 에디터와 런타임 게임 객체 모델을 연결시키려면 월드 덩어리를 메모리에 불러오고, 필요 없어지면 다시 내릴 방법이 있어야 한다.
게임 월드 로딩 시스템은 두 가지 중요한 역할을 맡는다.
1. 게임 월드 청크와 기타 필요한 자원들을 디스크에서 메모리로 불러 오는 파일 I/O 관리 역할
2. 자원들에 필요한 메모리를 할당하고 해제 하는 것을 관리하는 역할
15.4.1 단순한 레벨 로딩
가장 직관적인 게임 로딩 방식은 한 번에 오직 하나의 게임 청크만 로딩되게 하는 것이다.
ㄴ 레벨이 전환될 때 플레이어는 로딩 스크린을 보고 있어야 한다.
ㄴ 이 방식의 월드 로딩 디자인에는 스택 기반 할당자가 어울린다.
게임이 맨 처음 시작할 때 게임의 모든 레벨에 걸쳐 공통적으로 사용되는 자원들이 스택의 맨 아래에 불러온다. 이것을 LSR(load-andstay-resident) 데이터라고 부르며 스택에서 맨 아래 공간에 들어온다. LSR이 채워지는 공간 위의 메모리 공간에 게임 월드를 위한 데이터가 들어왔다 나갔다 한다.
15.4.2 심리스(Seamless : 끊김없는, 자연스러운) 로딩에 다가가기 : 에어 락
광활하고 연속된 seamless 월드를 구현하기 위한 방법이다.
게임 월드 자원을 위해 마련해 놓은 메모리를 똑같은 두 개의 블록으로 나누면 된다. 한쪽 블록에 레벨 A를 불러온 후 플레이어가 레벨 A에서 게임을 시작하게 하고, 그 후 스트리밍 파일 I/O 라이브러리(별도의 스레드에서 구현)를 사용해 다른 블록에 레벨 B를 불러오면 된다. 이 방식의 문제점은 월드에 할당할 수 있는 메모리가 기존의 절반으로 줄어든다는 점이다.
비슷하게 게임 월드 메모리를 두 개의 크기가 다른 블록으로 나누는 방법이 있다.
큰 블록에는 온전한 게임 월드 청크가 들어가고, 작은 블록은 아주 작은 청크가 겨우 들어갈 만큼의 크기를 가진다.(에어 락)
플레이어가 온전한 청크에서 에어 락에 들어갈 때, 플레이어는 문이나 다른 장애물 때문에 온전한 월드 지역을 보거나 다시 돌아가지 못한다. 이 와중에 온전한 청크를 메모리에서 내리고 새로운 온전한 크기의 월드 청크를 로딩한다.
결론적으로 에어락은 비동기 I/O를 이용해 플레이어가 로딩 스크린을 보지 않고도(게임 플레이가 끊기지 않고) 다음 단계의 월드 청크로 진행할 수 있게 해준다.
15.4.3 게임 월드 스트리밍
어떤 게임 디자인은 플레이어가 광활하고 이어져 있는 심리스 월드에서 플레이하고 있다는 느낌을 줘야 한다.
플레이어가 좁은 에어락 지역에 반복적으로 들어갈 필요가 없게 만들도록 해야 한다. 이를 위해 스트리밍이라는 기법을 사용한다.
여러개의 월드 청크를 메모리에 동시에 올려 놓고 캐릭터가 월드 청크에 진입하게 되거나 빠져나올 때 할당, 해제하는 방식으로 스트리밍을 구현할 수도 있는데, 크기가 큰 월드 청크를 스트리밍하는 대신에 전경(foreground), 메시, 텍스처, 애니메이션 등 모든 게임 자원을 같은 크기의 블록이 되게 쪼갤 수도 있다. 그런 후에 풀 기반 메모리 할당 시스템을 사용하면 메모리 단편화 걱정 없이 필요할 때 자원 데이터를 로딩하고 내릴 수 있다.
15.4.3.1 불러올 자원 결정
어떤 월드가 메모리에서 내려가면 플레이어는 절대 이 월드를 볼 수 없어야 하고,
어떤 월드가 메모리에 올라가 플레이어가 이 월드를 보기 전까지 충분한 시간을 두어야 한다.
15.4.4 객체 생성을 위한 메모리 관리
대부분의 게임 엔진에는 게임 객체들을 이루는 클래스들을 인스턴싱하고 필요 없어지면 파괴하는 일을 담당하는 게임 객체 생성(스포닝) 시스템이 있다.
동적 할당은 느린 과정이기 때문에 효율적으로 만들어야 한다.(메모리 단편화를 피해야 한다.)
15.4.4.1 객체 생성을 위한 오프라인 메모리 할당 사용
ㄴ 게임플레이 도중에 동적인 메모리 할당을 금지하는 방법
ㄴ 게임 객체들이 사용할 메모리를 월드 에디터가 오프라인에 할당할 수 있고, 월드 청크 데이터 안에 포함시킬 수 있다.
ㄴ 한 청크 안의 모든 게임 객체들이 사용하는 메모리 양이 미리 계산될 수 있기 때문에 메모리 단편화를 방지할 수 있다.
ㄴ 동적 객체 생성을 흉내내기 위해 월드에 필요한 수 만큼의 객체를 할당한 후 객체의 상태를 잠들고 깨우는 방식을 사용한다.
15.4.4.2 객체 생성을 위한 동적인 메모리 관리
타입이 다른 게임 객체들은 차지하는 메모리 크기가 다르기 때문에 흔히 많이 쓰이는 단편화 없는 풀 할당자를 사용할 수 없게 된다.
게임 객체들은 보통 생성된 순서와 상관 없이 파괴되기 때문에 스택 할당자도 사용할 수 없다.
남은 것은 힙 할당자 뿐인데 이는 단편화에 취약하다. 그래도 대안이 몇가지 있다.
1. 객체 타입마다 하나의 메모리 풀 사용
ㄴ 여러 풀을 관리해야 한다.
ㄴ 각 객체 타입별로 얼마나 많은 수를 사용할지 경험적으로 유추해야 한다.
2. 작은 메모리 할당자
ㄴ 관리해야 할 풀의 종류는 줄어들지만 풀마다 잠재적으로 낭비하는 메모리가 있을 수 있다.
ㄴ 할당 단위 크기가 두배로 늘어나는 메모리 풀들을 만들어 사용할 수 있다.
ㄴ 게임 객체를 위해 메모리를 할당할 때, 할당 단위가 객체의 크기와 같거나 이보다 큰 풀들 중에서 가장 작은 것을 찾는다.
3. 메모리 재배치
ㄴ 5.2.2.2 내용
15.4.5 게임 저장
월드 로딩 시스템과 저장 시스템은 비슷하지만 요구 조건이 다르기 때문에 따로 구현된다.
월드 청크는 월드 안에 있는 모든 동적 객체의 초기 상태를 담고 있고, 이에 더해 모든 정적 월드 요소에 대한 완전한 정보도 담는다.
배경 메시와 충돌 데이터 등의 정적 정보는 디스크 공간을 많이 차지하는 경우가 보통이기 때문에 월드 청크는 여러 파일로 구성되는 경우도 있고 연관된 데이터의 크기도 대개 크다.
게임 저장파일도 게임 객체의 현재 상태를 저장해야 하지만, 월드 청크에서 알 수 있는 정보들은 중복 저장할 필요가 없다.
그래서 게임 저장 파일은 월드 청크 파일보다는 훨씬 작으며, 데이터 압축과 생략에 더 중점을 둘 수 있다.
15.4.5.1 체크 포인트
체크 포인트라고 불리는 특정한 지점에서만 저장할 수 있게 하는 방식
체크 포인트 주변의 월드 청크에 게임의 상태가 저장된다.
어느 누가 플레이하든 데이터가 거의 비슷하기 때문에 따로 저장할 데이터가 적다. = 저장 파일의 크기가 작다.
15.4.5.2 자유 저장
게임 플레이 도중에 언제든 게임 상태를 저장할 수 있는 기능
게임 저장 파일의 크기가 커진다.
게임 저장 파일의 내용이 월드의 정적인 요소들만 빼고 월드 청크의 정보와 비슷하다.
15.5 객체 참조와 월드 질의
게임 객체의 id는 게임 내의 객체들을 구분하거나 런타임 검색, 객체 간 통신에서 수신자 명시 등에 쓰인다. 월드 에디터 툴에서도 검색과 구분을 위해 흔히 쓰인다.
런타임에는 게임 객체들을 검색할 수 있는 기능이 반드시 있어야 한다. 고유 id로 객체를 찾는 경우도 있고, 객체 타입이나 아니면 다른 조건으로 검색하는 경우도 있다.
질의를 통해 찾은 객체는 포인터, 핸들, 스마트 포인터를 이용해 참조를 구현할 수 있다.
15.5.1 포인터
객체 참조를 구현하는데 가장 빠르고 효율적이고 다루기도 쉽지만, 프로그래머에게 포인터 관리에 대한 부담이 클 수 있다. (실수하기 쉽다)
15.5.2 스마트 포인터
평상시에는 여느 포인터처럼 동작하지만 C/C++ 네이티브 포인터의 문제들은 해결한 작은 객체를 스마트 포인터라고 부른다.
포인터 역참조 기능을 지원하도록 *와 -> 연산자를 오버로딩해 원래 메모리 주소를 리턴한다.
ㄴ 3.1.3.6 내용
15.5.3 핸들
핸들의 문제점은 낡은 객체를 참조할 가능성이 있다는 점이다.
이 문제를 해결하려면 모든 핸들에 객체의 고유 id를 포함하게 만들면 된다.
// 핸들을 사용하여 객체를 참조할 때의 객체 클래스
class GameObject
{
private:
//..
GameObjectId m_uniqeud; // 객체의 고유 id
U32 m_handleIndex; // 핸들 생성을 빠르게 하기 위해
friend class GameObjectHandle; // id와 index에 접근할 수 있게
public:
GameObject()
{
// 고유 id는 월드 에디터에서 올 수도 있고,
// 아니면 런타임에 동적으로 할당할 수도 있다.
m_uniqueId = AssignUniqueObjectId();
// 핸들 테이블에서 비어있는 첫 번째 슬롯을 찾아 핸들 인덱스를 얻어온다.
m_handleIndex = FindFreeSlotInHandleTable();
}
};
// 글로벌 핸들 테이블
static GameObject* g_apGameObject[MAX_GAME_OBJECTS];
// 간단한 객체 핸들 클래스
class GameObjectHandle
{
private:
U32 m_handleIndex; // 핸들 테이블의 인덱스
GameObjectId m_uniquewId; // 낡은 핸들을 방지하기 위한 고유 id
public:
explicit GameObjectHandle (GameObject& object):
m_handleIndex(object.m_handleIndex),
m_uniqueId(object.m_uniqueId)
{ }
// 핸들을 역참조하는 함수
GameObject* ToObject() const
{
GameObject* pObject = g_apGameObject[m_handleIndex];
if(pObject != NULL && pObject -> m_uniqueId == m_uniqueId)
{
return pObject;
}
return NULL;
}
};
15.5.4 게임 객체 질의
게임 개발 중에 어떤 타입의 질의가 가장 많이 필요할지를 미리 결정한 다음 그 질의를 수행하는데 가장 효율적인 자료구조를 구현한다.
고유 id로 게임 객체 찾기
ㄴ 게임 객체를 가리키는 포인터나 핸들을 해시 테이블에 넣거나 이진 검색 트리에 넣고 고유 id로 찾는다.
특정 조건을 충족하는 모든 게임 객체를 순회하기
ㄴ 게임 객체들을 여러 조건에 맞춰 정렬한 후 연결 리스트에 미리 저장해 둔다.
ㄴ 예를 들면 특정 타입의 모든 게임 객체를 리스트로 만들거나 플레이어로부터 특정 범위 안에 있는 모든 객체를 리스트로 만들 수 있다.
발사체의 이동 경로에 있거나 어떤 목표 지점까지의 위에 있는 모든 객체를 찾기
ㄴ 보통 충돌 시스템을 활용한다. 충돌 시스템은 레이 캐스트나 형상 캐스트를 통해 충돌하는 물체를 빠르게 찾아낼 수 있다.
15.6 실시간 게임 객체 업데이트
대부분의 로우레벨 엔진 하부 시스템(렌더링, 애니메이션, 충돌, 물리, 오디오 등)은 주기적으로 업데이트해야 하는데, 게임 객체도 마찬가지다. 모든 게임 엔진은 메인 게임 루프의 일부분에서 게임 객체 상태를 업데이트 한다고 말할 수 있다.
15.6.1 단순한 접근 방식(하지만 동작하지 않는 방식)
게임 객체들 전체의 상태를 업데이트하는 가장 단순한 방법은 순회하면서 가상 함수, 즉 Update() 같은 함수를 모든 객체마다 차례대로 호출하는 것이다.
게임 객체들은 Update() 함수를 오버라이딩하고 이전 프레임과의 시간차를 인자로 전달 받는다.
모든 게임 객체를 아우르는 집합을 어떻게 관리할 것인가?
Update() 함수가 해야 할 일은 어떤 것인가?
15.6.1.1 활성 게임 객체의 집합 관리
활성(active) 게임 객체를 관리하는 데는 주로 싱글턴 매니저 클래스를 쓰는 경우가 많으며, 대개 GameWorld나 GameObjectManager 등의 이름을 갖는다.
게임 객체들은 게임이 진행되면서 생성되고 파괴되기 때문에 게임 객체의 집합은 보통 동적인 경우가 대부분이다.
대부분의 게임 객체는 게임 객체를 관리하는 데 단순한 연결 리스트보다는 좀 더 복잡한 자료 구조를 사용한다.
15.6.2 성능 조건과 일괄 업데이트
대부분의 상용 게임 엔진들은 게임 루프에서 각 엔진 하부 시스템을 직접 혹은 간접적으로 업데이트하며, 게임 객체의 Update() 함수 안에서 하지 않는다.
게임 객체가 어떤 하부 시스템을 이용할 일이 있으면 직접 하부 시스템에 요청해 자신을 위한 상태 정보를 할당하게 만들 수 있다.
virtual void Tank::Update(float dt)
{
// 탱크의 상태를 업데이트 한다.
MoveTank(dt);
DeflectTurret(dt);
FireIfNecessary();
// 여러 엔진 하부 시스템의 속성을 설정하지만
// 직접 업데이트 하지는 않는다.
// 조건에 따라 객체에 대한 하부 시스템의 속성을 다르게 설정
if(justExploded)
{
m_pAnimationComponent-> PlayAnimation("explode");
}
if(isVisible)
{
m_pCollisionComponent->Activate();
}
else
{
m_pCollisionComponent->Deactivate();
}
}
// 게임 루프
while (true)
{
PollJoypad();
float dt = g_gameClock.CalculateDeltaTime();
// 모든 게임 객체의 update()
// 여러 엔진 하부 시스템들의 속성들만 설정
for (each gameObject)
{
gameObject.Update(dt);
}
// 하부 시스템들의 일괄 업데이트
g_animationEngine.Update(dt);
g_physicsEngine.Simulate(dt);
g_collisionEngine.DetectAndResolveCollision(dt);
g_audioEngine.Update(dt);
g_renderingEngine.RenderFrameAndSwapBuffers();
}
일괄 업데이트가 가져올 수 있는 성능 이득
1. 최대의 캐시 일관성
ㄴ 객체들의 데이터를 연속된 RAM 공간에 모을 수 있다.
2. 계산 중복 최소화
ㄴ 공통적인 연산은 한 번만 하고 매 객체는 다시 이것을 계산하기보다는 계산된 것을 가져와 활용하면 된다.
3. 자원 재할당 감소
ㄴ 엔진 하부 시스템들은 업데이트하는 동안 메모리를 비롯한 자원을 할당하고 관리하는 일을 하는 경우가 많은데, 일괄 업데이트를 사용하면 프레임에서 한 번만 자원을 할당한 후 모든 객체에서 돌아가며 재활용할 수 있다.
4. 효율적인 파이프라인화
ㄴ 엔진 하부 시스템들은 게임 월드에 있는 모든 객체마다 근본적으로 동일한 연산을 수행하는 경우가 많다. 일괄 업데이트를 이용하면 새로운 최적화를 가능하게 할 수도 있고 전용 하드웨어 자원을 활용하는 것도 가능하다.
동적 강체들이 존재하는 시스템에서 충돌을 해결할 때 객체 간의 교차를 해결하려면 객체들을 그룹으로 처리해야 한다. 일괄 업데이트는 이런 면에서도 필수적이다.
15.6.3 객체와 하부 시스템 간 상호 의존
객체간 의존 관계가 있거나 엔진 하부 시스템끼리 의존 관계가 있는 경우 업데이트의 순서를 적절하게 조절해야 한다.
15.6.3.1 단계적 업데이트
하부 시스템 간 의존성을 제대로 처리하려면 메인 게임 루프 안에서 하부 시스템들이 올바른 순서대로 업데이트하도록 명확하게 코드를 짜면 된다. 예를 들어 래그 돌 물리 시스템이 상호작용하는 과정을 올바로 처리하려면 다음과 같이 코드를 짤 수 있다.
while(true) // 메인 게임 루프
{
//
/* 모든 객체의 상태 설정 */
//
g_animationEngine.CalculateIntermediatePoses(dt); // 중간 단계의 로컬 공간 뼈대 포즈를 계산
g_ragdollSystem.ApplySkeletonsToRagDolls(); // 월드 공간으로 다시 계산하여 물리 시스템 내의 연결된 강체들에 적용
g_physicsEngine.Simulate(dt); // 래그 돌의 물리 시뮬레이션 동작
g_collisionEngine.DetectAndResolveCollisions(dt);
g_ragdollSystem.ApplyRagDollsToSkeletons();
g_animationEngine.FinalizePosAndMatrixPalette(); // 최종 월드 공간 포즈를 계산하고 스키닝 행렬 팔레트를 생성
// ..
}
이렇게 게임 객체들이 다양한 엔진 하부 시스템의 중간 결과에 의존하는 경우 게임 객체들의 상태를 언제 업데이트 해야 하는지를 고민해야 한다.
15.6.3.2 버킷 업데이트
객체간 의존성이 있는 경우 문제가 생길 수 있다.
객체 B가 객체 A의 손에 들려 있어서 A가 완전히 업데이트 된 후(최종 월드 공간 포즈와 행렬 팔레트까지 계산이 끝난 후)에야 B를 업데이트 할 수 있다고 가정한다.
해결하는 방법은 객체들의 의존성 트리를 만들어 의존이 없는 객체(첫번 째 버킷) 들의 그룹(버킷)들의 완전한 게임 객체 업데이트 및 엔진 시스템을 업데이트 한 뒤 차례대로 버킷을 업데이트 하는 것이다.
의존성 트리들의 깊이를 제한하는 게임이 많다. (고정된 수의 버킷을 쓰기 위해서)
15.6.3.3 불완전한 객체 상태와 한 프레임 차 랙
게임 객체의 상태는 순차적으로 설정되기 때문에 루프 중간에 프로그램을 잠시 멈추면 어떤 객체들은 업데이트 되지 않아, S(t₁)로, 어떤 객체들은 업데이트 되어 S(t₁ + Δt)의 상태를 갖게 된다. 또한 루프의 어느 지점이느냐에 따라 객체들이 모두 일부만 업데이트된 상태일 수도 있다. 예를 들어 애니메이션 포즈 블렌딩은 이미 수행했지만, 물리와 충돌 과정은 아직 계산되지 않았을 수도 있다.
게임 객체들의 상태는 업데이트 루프 전과 후에는 일관되지만 루프 도중에는 일관되지 않을 수도 있다는 뜻이다.
문제는 주로 업데이트 루프 안에서 게임 객체들이 서로의 상태 정보를 질의할 때 발생한다.
이런 문제를 해결하려면 게임 객체를 버킷으로 그룹 짓는 방법도 있지만 객체 상태 캐싱이라는 기법을 사용할 수도 있다.
15.6.3.4 객체 상태 캐싱
새로운 객체 상태 벡터 S(t₂)를 계산하기 전에 이전 객테 상태 벡터 S(t₁)을 복사(캐싱)해 놓게 만드는 방법이다.
ㄴ 이전 객체이기 때문에 업데이트 순서에 상관이 없고 일관성이 있다.
ㄴ 이전 시각과 현재 시각 사이의 아무 때라도 두 상태를 선형 보간하여 얻을 수 있다. (하복의 물리엔진은 모든 강체의 이전 상태와 현재 상태를 모두 유지한다.)
단점
ㄴ 상태를 엎어 쓰는 방식에 비해 메모리를 두 배 더 차지한다.
ㄴ 이전 시각 t₁의 상태는 일관되지만 현재 시각 t₂의 상태는 일관성 문제가 발생할 수 있다.
15.6.4 병렬성을 위한 디자인
게임 객체 상태를 업데이트 하는 방식에 어떤 방식으로 병렬화를 사용할 것인가?
15.6.4.1 게임 객체 모델 자체를 병렬화
게임 객체 모델은 병렬화하기 어렵다.
ㄴ 게임 객체들은 서로 의존성이 강하다.
게임 객체 모델을 병렬화 한다면
ㄴ 모든 게임 객체들은 다른 게임 객체들을 참조할 수 없어야 한다. 모든 객체 간 통신은 메세지 전달 방식을 써야 하며, 객체들이 완전히 다른 메모리 영역에 있거나 물리적으로 분리된 CPU 코어에서 처리되고 있는 상황에서도 효율적인 객체 간 메세지 전달 시스템을 구현해야 한다.
15.6.4.2 병행 구조인 엔진 하부 시스템과의 인터페이스
일반적으로는 게임 객체 모델을 병렬화하는 방법보다는 로우레벨 엔진 시스템들을 병렬화하는 데 노력을 기울인다.
ㄴ 로우레벨 하부 시스템이 게임 객체 모델 처리보다 CPU를 더 많이 쓰기 때문
싱글 스레드 게임 객체 모델을 사용하더라도 병렬적으로 실행되는 엔진 하부 시스템과 상호작용을 해야 한다.
ㄴ 게임 객체들을 업데이트 할 때 블로킹 함수를 호출하면 안 된다.
ㄴ 작업 요청을 가장 일찍 보낼 수 있는 때는 언제인가? (일찍 보내야 필요할 때 완료되어있을 가능성이 커진다.)
ㄴ 작업 요청을 보내고 얼마나 오래 기다릴 수 있는가? (업데이트 루프 뒷부분까지 기다리거나 몇 프레임 랙이 생겨도 상관 없어서 이전 프레임의 결과로 현재 프레임의 객체 상태를 업데이트 할 수도 있다.)
15.7 이벤트와 메세지 전달
게임은 본질적으로 이벤트 주도 방식으로 돌아간다.
이벤트가 발생했음을 연관있는 게임 객체들에게 알려주고, 그 객체들이 이벤트에 반응할 수 있게 도와 줄 방법이 필요하다.
15.7.1 정적 타입 함수 바인딩의 문제점
게임 객체에게 이벤트가 발생했음을 알리는 가장 간단한 방법은 그 객체의 멤버함수를 부르는 것이다.
정적 타입 가상 함수를 이벤트 핸들러로 사용하려면 베이스 클래스 GameObject에는 게임에서 발생 가능한 모든 이벤트 핸들러를 가상 함수로 선언해야 한다. (데이터 주도 방식으로 이벤트를 생성할 수 없다.)
이벤트에 관심이 있는 일부 객체 타입이나 일부 인스턴스들만 그 이벤트를 받게 하고 싶어도 방법이 없다.
동적 타입 함수 바인딩을 사용하면 된다. 이 기능을 기본적으로 지원하는 프로그래밍 언어도 있다. (C#) 그렇지 않은 경우에는 직접 코드를 짜야 한다.
보통 데이터 주도형 접근 방식으로 구현한다. (함수 호출 개념을 객체로 캡슐화한 후 이것을 런타임에 전달하는 방식)
15.7.2 이벤트를 객체에 캡슐화
이벤트는 두 가지 구성 요소로 이뤄진다. 하나는 타입(폭발, 아군의 부상, 체력 아이템을 집어 들기) 이고, 다른 하나는 전달 인자이다.
공통된 루트 이벤트 클래스를 상속하여 여러 다른 타입의 이벤트를 구현하기도 한다.
이벤트 (메세지) 를 객체로 캡슐화 하면 여러 이득이 있다.
ㄴ 단일 이벤트 핸들러 함수 : 모든 타입의 이벤트를 처리할 가상 함수 한 개만 있으면 된다. (virtual void OnEvent(Event& event);)
ㄴ 영속성 : 함수 호출에서는 함수가 리턴하고 나면 인자들을 사용할 수 없지만 이벤트 객체는 타입과 전달 인자를 데이터로 저장하기 때문에 영속성이 있다.
ㄴ 이벤트 전달의 임의성 : 객체는 연관된 객체에 이벤트를 전달할 수 있는데 전달하는 객체는 이벤트에 대해 몰라도 된다.
struct Event
{
const U32 MAX_ARGS = 8;
EventType m_type;
U32 m_numArgs;
EventArg m_aArgs[MAX_ARGS];
};
이벤트/메세지/명령을 객체로 캡슐화 한다는 개념은 명령 패턴이라고 불린다.
https://lemonyun.tistory.com/66
15.7.3 이벤트 타입
// 이벤트 타입을 열거형 타입으로 정의해 구분하는 방법
enum EventType
{
EVENT_TYPE_LEVEL_STARTED,
EVENT_TYPE_PLAYER_SPAWNED,
EVENT_TYPE_ENEMY_SPOTTED,
// ...
};
이 방식의 문제점
1. 게임 전체에 쓰이는 모든 이벤트 타입에 대한 정보가 한 곳에 집중돼 있음
2. 이벤트 타입이 하드 코딩되어 있기 때문에 데이터 주도 방식으로 쉽게 추가할 수 없음
3. 열거형 값은 단순한 인덱스에 불과해서 순서에 영향을 받는다.
ㄴ 이벤트 타입을 문자열로 인코딩하는 방법을 사용하면 해결할 수 있다.
15.7.4 이벤트 전달 인자
이벤트 전달 인자를 구현하는 방법
1. 각 이벤트 타입마다 새 이벤트 클래스를 상속하고 전달 인자는 새 이벤트 클래스의 데이터 멤버로 하드 코딩
2. variant의 집합으로 저장하는 방법
ㄴ 고정 크기의 작은 배열로 구현
ㄴ 동적 크기 배열이나 연결 리스트를 이용해 구현
3. 키-값 쌍을 이벤트 전달 인자로 사용
ㄴ 이벤트를 보내는 쪽과 받는 쪽에서 전달 인자의 순서를 정확하게 알고 있어야 하기 때문에 등장한 개념(버그가 발생하기 쉬움)
15.7.5 이벤트 핸들러 (OnEvent 함수)
게임 객체가 이벤트를 받으면 그 이벤트에 반응해야 한다. 이 과정을 이벤트를 핸들링한다고 하고, 보통 이벤트 핸들러라고 부르는 함수나 스크립트 코드에서 처리한다.
이벤트 핸들러는 대부분 모든 타입의 이벤트를 처리할 수 있는 네이티브 가상 함수나 스크립트 함수로 구현하는 경우가 많다.
virtual void SomeObject::OnEvent(Event& event)
{
switch (event.GetType())
{
case EVENT_ATTACK:
RespondToAttack(event.GetAttackInfo());
break;
case EVENT_HEALTH_PACK:
AddHealth(event.GetHealthPack().GetHealth());
break;
//...
//이 객체에서 처리하지 않는 이벤트
default:
break;
}
}
15.7.7 책임 연쇄 패턴
게임 객체들끼리는 거의 항상 의존성이 있다.
일반적으로 게임 객체들 간의 상호 연관성은 하나 이상의 관계 그래프로 생각할 수 있다.
이와 같은 관계 그래프에서 연결된 객체들 간에 당연히 이벤트를 전달할 수 있어야 한다. 여러 개의 구성요소로 이뤄진 게임 객체가 이벤트를 받으면 구성 요소 모두에 이벤트를 전달해 처리할 기회를 줘야 하는 경우도 있다.
객체들로 이뤄진 그래프 내에서 이벤트를 전달하는 기법은 책임 연쇄 패턴이라고 불리기도 한다.
이벤트가 전달되는 순서는 보통 엔지니어가 정하며, 이벤트의 체인이 첫 번째 객체에 전달하면 그 객체의 이벤트 핸들러에서 처리될 수 있는지 판단한다. (switch문의 case에 걸리지 않으면 다음 객체에 전달)
15.7.8 관심 이벤트 등록
이벤트를 브로드캐스팅하면 모든 게임객체의 이벤트 핸들러를 호출해야 한다. (비효율적)
게임 객체들이 특정 이벤트에 관심 등록을 할 수 있게 하는 방법
1. 이벤트 타입마다 관심 등록한 게임 객체를 연결 리스트로 두기
2. 게임 객체에 이벤트를 나타내는 비트 배열을 두고 관심 있는 것만 비트를 켜는 방법
15.7.9 이벤트 큐 사용 여부
15.7.9.1 이벤트 큐 방식의 몇 가지 장점
이벤트 처리 시점 제어
ㄴ 이벤트들은 게임 루프 안에서 어느 시점에 처리되는지에 민감한 영향을 받는 경우가 있다.
미래에 이벤트를 보내기
ㄴ 큐에 넣기 전에 원하는 전달 시각을 이벤트에 기록한다. 그리고 현재의 게임 클록이 이벤트의 전달 시각과 같거나 이미 지났을 경우만 이벤트를 처리하게 한다. 큐의 이벤트들을 전달 시각이 이른 순으로 정렬해 놓으면 이런 식으로 동작하도록 구현하기 수월하다.
이벤트 우선 순위
ㄴ 여러 이벤트가 같은 전달 시각을 갖는 경우 이벤트의 우선순위 레벨을 주어 해결할 수 있다.
15.7.9.2 이벤트 큐 방식의 문제점
이벤트 시스템의 복잡도 증가 (구현 난이도, 유지보수 비용 증가)
이벤트와 전달 인자를 깊은 복사해야 하는 문제
깊은 복사는 동적 메모리 할당(잠재적인 속도 저하와 메모리 단편화 문제)를 해야 할 수 도있게 만든다.
15.7.10 이벤트를 즉시 보낼 때의 문제점 (큐를 사용하지 않는 경우의 문제점)
엄청나게 깊은 콜 스택을 유발할 수 있다.
15.7.11 데이터 주도 이벤트/메세지 전달 시스템
이벤트 시스템을 데이터 주도 방식으로 구현하면 이벤트를 보내고 받는 과정을 수정할 권한을 디자이너에게 줄 수 있다.
월드 에디터에서 디자이너는 객체를 선택하고 그 객체가 특정 이벤트에 어떻게 반응할지 (어떤 핸들러를 사용할지) 설정할 수 있다.
엔진에서 게임 디자이너가 쓸 수 있게 간단한 스크립트 언어를 지원하는 형태가 있다. 디자이너는 특정 타입의 객체가 특정 타입의 이벤트에 어떻게 반응할지를 코드로 짤 수 있다.
15.7.11.1 데이터 통로 통신 시스템
모든 게임 객체들이 데이터 스트림을 연결할 수 있는 입력 포트를 하나 이상 갖고, 다른 객체들에 데이터를 보낼 출력 포트를 하나 이상 갖는다.
15.8 스크립트
스크립트는 바이트코드 패턴과 연관이 있다.
https://lemonyun.tistory.com/75
15.8.1 런타임 언어와 데이터 정의 언어
스크립트 언어가 사용되는 용도
1. 데이터 정의 언어
ㄴ 나중에 엔진에서 사용될 새 자료구조를 만들고 채워 넣는 것을 도와주는 것이 주된 목적
2. 런타임 스크립트 언어
ㄴ 런타임에 엔진 안에서 실행되는 스크립트 언어로, 대개 하드코딩된 엔진의 게임 객체 모델이나 다른 엔진 시스템을 확장하고 커스터마이즈하는데 쓰인다.
15.8.2.1 게임 스크립트 언어의 일반적 특성
인터프리트 방식
ㄴ 유연성, 이식성, 빠른 반복 생산을 위한 선택이다.
ㄴ 코드가 직접 CPU에서 실행되는 것이 아니라 가상 머신에서 실행되기 때문에 스크립트 코드를 실행하는 방법과 시점을 엔진에서 능동적으로 조정할 수 있다.
가벼움
ㄴ 가상머신은 메모리를 적게 먹는다.
편리성과 사용 편의성
ㄴ 스크립트 언어는 대개 특정 게임의 요구 사항에 맞게 커스터마이즈해 사용한다.
15.8.3 널리 쓰이는 게임 스크립터 언어
상용 혹은 오픈소스 언어를 고쳐 쓸 것인가 아니면 처음부터 새로 만들 것인가?
대부분의 경우 어느 정도 잘 알려지고 안정된 스크립트 언어를 골라 필요한 기능들을 추가해 가는 편이 더 편리하다.
15.8.3.1 QuakeC
퀘이크 엔진을 위해 만든 커스텀 스크립트 언어
15.8.3.2 언리얼스크립트
C++와 문법 구조가 유사하고 클래스, 지역변수, 루프, 배열, 구조체 등 C와 C++ 프로그래머가 익숙한 대부분의 개념을 지원한다.
클래스 계층 구조를 확장할 수 있는 능력
레이턴스 함수
언리얼에디터와의 편리한 연결
멀티플레이어 게임을 위한 네트워크 리플리케이션
15.8.3.3 루아
게임 스크립트 언어로서 제일 많이 쓰인다고 한다.
15.8.3.4 파이썬
15.8.3.5 폰/스몰/스몰-C
15.8.4 스크립트의 구조
스크립트 콜백
ㄴ 게임 루프에서 객체를 업데이트할 때 스크립트로 작성된 콜백 함수를 엔진에서 호출할 수 있다. 이를 통해 시간에 따른 게임 객체를 어떻게 업데이트 할 것인지 커스터마이징 할 수 있다.
스크립트로 짠 이벤트 핸들러
스크립트로 게임 객체 타입을 확장하거나 새 타입을 정의하기
스크립트로 구성 요소 및 속성 정의하기
스크립트 주도 엔진 시스템
ㄴ 게임 객체 모델 전부를 스크립트로 짜고, 네이티브 엔진 코드는 로우레벨 엔진 구성 요소를 사용해야 할 때만 호출하는 방식으로 구현한다.
스크립트 주도 게임
15.8.5 런타임 게임 스크립트 언어의 기능
대다수 게임에서 스크립트 언어를 사용하는 주된 이유는 게임플레이 기능을 구현하는 것이고, 이것은 게임의 객체 모델을 보강하거나 커스터마이즈하는 형태로 이뤄지는 경우가 일반적이다.
15.8.5.1 네이티브 프로그래밍 언어와의 인터페이스
게임 엔진이 스크립트 코드를 실행할 수 있어야 하고, 마찬가지로 스크립트 코드가 엔진에서의 작업을 시작하게 할 수 있어야 한다.
보통 런타임 스크립트 언어의 가상 머신은 게임 엔진 안에 들어간다. 엔진이 가상 머신을 초기화하고 필요한 때에 스크립트 코드를 실행하며, 스크립트가 실행되는 과정을 관리한다.
함수형 스크립트 언어의 주요 실행 단위는 함수다. 함수 이름이 위치한 바이트 코드를 찾은 후 가상 머신을 생성해 코드를 실행한다.
객체지향 스크립트 언어에서는 클래스가 주요 실행 단위다. 이런 시스템에서는 객체를 생성하거나 파괴하는 것이 가능하고, 각 클래스 인스턴스의 메서드를 호출할 수 있다.
15.8.5.2 게임 객체 핸들
스크립트 함수가 게임 객체를 다뤄야 하는 때도 있는데 스크립트 코드에서 게임 객체를 참조할 방법은 여러가지가 있다.
임의의 숫자로 이뤄진 핸들을 사용하는 방법이 있다. (혹은 문자열, 해시 문자열 id)
15.8.5.3 이벤트 받기와 처리
이벤트는 보통 개별 객체에 전달되고 그 객체 내에서 처리된다. 그렇기 때문에 스크립트로 짠 이벤트 핸들러를 어떤 식으로든 객체와 연관 지을 방법이 있어야 한다.
15.8.5.4 이벤트 보내기
15.8.5.5 객체지향 스크립트 언어
15.8.5.6 스크립트로 짠 유한 상태 기계
15.8.5.7 스크립트 멀티스레드
'읽은 책 > 게임 엔진 아키텍처' 카테고리의 다른 글
14. 게임플레이 시스템의 소개 (0) | 2022.08.17 |
---|---|
12. 충돌과 강체 역학 (0) | 2022.08.17 |
11. 애니메이션 시스템 (0) | 2022.08.13 |
10. 렌더링 엔진 (0) | 2022.08.12 |
9. 디버깅과 개발 도구 (0) | 2022.08.10 |
14. 게임플레이 시스템의 소개
14.1 게임월드의 구조
14.1.1 게임 월드의 요소
대부분의 비디오 게임들은 2차원 또는 3차원의 가상 게임월드에서 이루어지는데, 이 가상 월드는 여러 가지 개별적인 요소들로 구성된다. 일반적으로 이 요소들은 크게 정적요소와 동적요소로 나눌 수 있다.
정적 요소는 지형, 건물, 도로, 교량 등과 같이 움직이지 않거나 게임플레이와 적극적인 상호작용을 하지 않는 것들을 의미한다.
나머지들은 다 동적 요소이다.
일반적으로 게임플레이는 주로 동적 요소를 다루게 된다.
게임월드의 동적요소 전체를 나타내는 용어로서 게임 상태 (game state)라는 용어를 사용하기로 하자
정적요소와 동적요소를 구분하게 되면 최적화에 활용할 수 있다. 예를 들어 어떤 메쉬가 정적이어서 움직이지 않는다면 정적 정점 조명, 조명 맵, 그림자 맵, 정적 환경 차폐 정보, PRT, 구면 조화 함수 계수를 사용한 방식으로 조명을 미리 계산할 수 있다.
14.1.1.1 정적 기하
정적 요소의 기하는 주로 마야(Maya)와 같은 도구를 사용해 만드는데, 커다란 하나의 삼각형 메시로 만들거나 또는 여러 개의 작은 조각으로 분해하여 만들게 된다. 화면의 정적인 부분은 인스턴싱된 기하를 이용해 만드는 경우도 있다.
정적 시각요소와 충돌 데이터(충돌 기본 단위)는 브러시 기하를 이용하여 만들 수 있다. 브러시는 여러 볼록 다면체들이 모여서 만들어진 모양을 의미하며, 브러시 기하는 빠르고 만들기 쉬우며 BSP 트리를 기반으로 하는 렌더링 엔진에 잘 통합될 수 있다.
14.1.2 월드 청크
게임 월드는 메모리의 제한, 게임흐름 조절의 필요성, 개발 과정에서 분업의 필요성 등의 이유에 따라 청크로 나누어진다.
14.1.3 하이레벨 게임흐름
하이레벨 게임흐름이란 플레이어의 목표를 선형적으로 또는 트리나 그래프와 같은 형태로 정의하는 것이다.
14.2 동적요소 구현하기 : 게임 객체
게임 월드 내의 동적요소들을 게임 객체라고 부르기로 한다.
게임 객체는 개체(entities), 액터(actors), 에이전트 (agent) 등 여러 가지 이름으로 불리고 있다.
게임 객체는 보통 타입에 따라 분류된다. 타입이 다른 객체들은 속성 스키마와 행위도 서로 다르다.
14.2.1 게임 객체 모델
게임 객체 모델은 특정 게임을 구성하는 개체들을 시뮬레이트하기 위하여 사용되는 구체적인 객체지향 프로그래밍 인터페이스다.
게임 객체 모델은 게임 엔진을 만들 때 사용한 프로그래밍 언어를 확장하는 경우가 많다.
ㄴ C++언어와 같은 객체지향 언어로 만들어진 게임의 경우에도 리플렉션, 지속성, 네트워크 복제 같은 고급 기능들을 추가할 수 있다.
ㄴ 게임 객체 모델은 여러 언어의 기능들을 혼합하기도 한다. 예를 들어 C언어 C++ 언어같이 컴파일이 필요한 프로그래밍 언어와 Python, Lua, Pawn 같은 스크립트 언어를 통합하여 양쪽 언어에서 모두 사용할 수 있는 단일화된 객체 모델을 제공할 수도 있다.
14.2.2 툴 측면 설계와 런타임 설계
기획자가 월드 에디터를 통해 보는 객체 모델이 런타임에 게임을 구현하기 위해 사용되는 객체 모델과 다를 수도 있다.
ㄴ 툴 측면 게임 객체 모델은 런타임에 C언어와 같은 객체지향 기능이 없는 언어로 구현될 수도 있다.
ㄴ 직관적으로는 하나의 툴 측면 게임 객체가 런타임에 하나의 클래스로 구현되어야 할 것 같지만 필요에 따라 여러 클래스의 모음으로 구현될 수도 있다.
ㄴ 툴 측면 게임 객체는 런타임에 고유한 id 하나가 되는 경우도 있다.
툴 측면 객체 모델은 기획자가 월드 에디터를 사용하면서 보게 되는 게임 객체 타입들이다.
런타임 객체 모델은 프로그래머가 툴 측면 객체 모델을 구현할 때에 사용한 프로그래밍 언어와 소프트웨어 시스템으로 규정된다.
14.3 데이터 주도 게임 엔진
데이터 주도 게임 엔진이라는 것은 게임의 전체 혹은 일부가 프로그래머가 개발한 소프트웨어 보다는 아티스트나 기획자가 만들어 낸 데이터에 의해 조정된다는 것을 의미한다.
ㄴ 모든 팀원들의 잠재력을 최대한 이끌어내고, 엔지니어들의 부담을 경감시킬 수 있기 때문에 개발팀의 효율을 개선할 수 있다.
ㄴ 게임 기획자와 아티스트들에게 데이터 주도적인 방식으로 게임 콘텐츠를 만들 수 있는 툴이 제공되어야 한다.
ㄴ 게임 기획자와 아티스트들이 자신들의 작업을 게임 속에서 미리 살펴보고 문제를 해결할 수 있는 도구도 제공되어야 한다.
데이터 주도 구조가 특정 게임을 기획하는데 미치는 영향이나 팀원들의 구체적인 요구사항들을 고려해보지도 않은 채 데이터 주도 구조를 만들려고 시도한다면 필요 이상으로 목표를 높게 잡거나, 너무 복잡하고 사용하기 힘든 툴과 엔진 시스템을 만들게 될 수 있다.
14.4 게임월드 에디터
게임 플레이 공간에서 게임 월드 청크를 정의하고, 정적 요소와 동적 요소들을 생성하는 툴에 해당하는 것이 게임월드 에디터이다.
모든 상용 게임엔진에는 어떤 형태로든 월드 에디터 툴이 포함되어 있다.
일반적으로 게임월드 에디터에서는 속성값과 같은 게임 객체의 초기 상태를 지정할 수 있다. 또한 사용자가 게임월드 내에서 동적요소의 행동을 조절할 수도 있다. 이러한 작업은 데이터 주도적 설정 파라미터를 이용할 수도 있고, 스크립트 언어를 이용할 수도 있다.
14.4.1 게임월드 에디터의 일반적인 기능
14.4.1.1 월드 청크의 생성과 관리
게임월드는 보통 청크 단위로 만든다.
게임월드 에디터를 사용하여 새로운 청크를 생성할 수 있고, 기존의 청크에 대하여 이름을 바꾸거나 분해하거나 합치거나 없앨 수 있다. 각 청크들은 여러 개의 정적 메시나 AI 네비게이션 맵, 잡을 수 있는 물체에 대한 정보, 방어 지점에 대한 정보 등과 같은 여러 정적요소들과 연결될 수 있다.
하나의 청크를 하나의 배경 메쉬로 정의하는 엔진에서는 배경 메쉬가 없이는 청크를 만들지 못한다.
어떤 게임 엔진에서는 청크를 AABB, OBB, 임의의 다격형 영역과 같은 경계 볼륨으로 청크를 정의하고 그 안에 여러 개의 메시를 넣는다.
월드 에디터 중에는 지형이나 물과 같은 특별한 정적요소들을 저작하기 위한 전용 도구가 있는 것도 있다.
14.4.1.2 게임월드의 시각화
게임월드 에디터의 사용자(기획자, 아티스트)에게는 게임월드 콘텐츠를 시각화 하는 것이 중요하다. 그래서 대부분의 게임 월드 에디터에서는 3차원 투영기능이나 2차원 직각투영 기능을 제공한다.
어떤 에디터에서는 전문 렌더링 엔진을 직접 포함하고 있어서 이러한 시각화 기능을 직접 제공하기도 하고, 또 다른 에디터는 maya나 3ds Max와 같은 3D기하 에디터의 뷰포트 기능을 이용하여 시각화 기능을 제공하기도 한다.
14.4.1.3 네비게이션
사용자가 게임월드 내에서 물체를 배치하기 위해서는 카메라를 이동시킬 수 있어야 한다.(네비게이션)
직각 투영, 스크롤, 줌인/아웃, 특정 객체 주위 회전 기능이 필요할 수 있다.
14.4.1.4 선택
한 번에 하나의 객체를 선택할 수 있는 에디터도 있지만 더 발전된 에디터는 여러 객체들을 한번에 선택할 수 있다.
객체를 선택할 때는 직각 투영 화면에서 고무줄 박스를 이용할 수도 있고, 3D 화면에서 레이 캐스트 선택 기능을 이용할 수도 있다.
3D 화면에서 레이 캐스트를 이용하여 객체를 선택할 때 가장 가까운 객체를 선택하여 주는 대신 레이와 교차되는 객체들을 순서대로 번갈아 가면서 선택하여 주는 기능을 제공하는 에디터도 있다.
모든 월드 요소들의 이름을 스크롤 할 수 있는 형태의 리스트나 트리 형태로 보여주어 객체를 선택할 수 있도록 하는 에디터들도 있다.
14.4.1.5 계층
게임 객체들을 모아서 계층별로 관리하면 게임 월드의 컨텐츠들을 잘 정리할 수 있다.
계층별로 화면에서 보이거나 보이지 않게 설정할 수도 있고, 계층별로 색깔을 부여해 구별할 수도 있다.
분업을 할 때도, 계층을 이용할 수 있다.
각 계층들을 개별적으로 불러오거나 저장할 수 있는 에디터에서는 여러 사람이 동시에 하나의 월드 청크에 대한 작업을 하더라도 충돌이 발생하지 않도록 할 수 있다.
14.4.1.6 속성 그리드
게임 월드 청크에 있는 정적 및 동적요소에는 사용자가 수정할 수 있는 여러 가지 속성이 있다.
단순한 키-값의 쌍으로 정의되며 대부분의 에디터에서는 스크롤 할 수 있는 속성 그리드 창을 이용하여 현재 선택된 객체의 속성들을 보여준다. 사용자는 속성 그리드 창에서 직접 타이핑을 하거나 체크박스, 드롭다운 콤보박스를 이용하거나 스피너 컨트롤을 상하로 드래깅하여 값을 수정할 수도 있다.
14.4.1.7 객체의 위치 정하기와 정렬시키기
객체의 이동, 회전, 크기 변화 기능을 제공해야 한다.
객체를 특정위치에 배치하거나 정렬시키는 기능을 제공한다.
ㄴ 자석 기능 (snap to grid)
ㄴ 객체 기준으로 정렬하기 (align to object)
14.4.1.8 특수 객체 타입
조명
ㄴ 조명에는 메시가 없기 때문에 대개 특수한 아이콘을 이용하여 조명을 표현한다.
파티클 이미터
ㄴ 렌더링 엔진이 에디터와 독립되어 있다면 에디터에서 파티클을 시각화하는데 문제가 있다. 때문에 아이콘만을 사용해서 파티클 이미터를 보여준다.
음원
ㄴ 3D 렌더링 엔진은 음원도 3차원 입체로 모델링한다. 이러한 작업을 위해서 월드 에디터에서 전용 편집 툴을 제공하는 경우가 많다.
영역(트리거)
ㄴ 게임에서 객체가 어떤 공간에 들어가거나 나오는 등의 일이 발생했을 때에 이를 감지하거나 여러 가지 목적으로 공간을 나눌 때에 사용하는 입체 공간이다.
스플라인
ㄴ 스플라인이란 여러 개의 조절점들과 그 점에서의 접선 벡터로 정의되는 3차원 곡선이다.
ㄴ 대개는 접선 벡터 없이 조절점으로만 정의되고 곡선이 모든 조절점들을 지나는 특성을 가지는 Catmull-Rom 스플라인이 많이 사용된다.
ㄴ 월드 에디터에서는 스플라인을 구성하는 개별 조절점을 선택하여 조정할 수 있는 기능과 객체(곡선) 전체를 선택하는 기능이 필요하다.
AI를 위한 네브 메시(nav meshes)
ㄴ 많은 게임에서 NPC들은 경로 찾기 알고리즘을 사용하여 게임월드의 탐색 가능 영역을 돌아다닌다. 이 때 AI 설계자들이 월드 에디터를 사용하여 탐색 가능 영역을 생성, 시각화, 수정한다. 예를 들어 네브 메쉬는 탐색 가능 영역의 경계를 쉽게 정의할 수 있도록 하는 2D 삼각형 메시로서 경로 검색기에게 연결 정보를 제공한다.
14.4.1.9 월드 청크의 저장 및 불러오기
불러오고 저장하는 월드 청크의 단위는 엔진마다 크게 다르다.
ㄴ 월드 청크 하나를 하나의 파일로 저장하는 엔진
ㄴ 계층을 개별적으로 저장하는 엔진
데이터 포맷도 엔진마다 다르다.
ㄴ 이진 포맷
ㄴ XML이나 JSON같은 텍스트 포맷
14.4.1.10 빠른 반복작업
어떤 에디터는 게임 속에서 직접 수행되면서 사용자가 수정한 효과를 즉시 확인할 수 있게 해준다.
오프라인으로 동작하는 에디터는 마야의 플로그인 형태로 동작하는 프로그램을 예로 들 수 있다.
'읽은 책 > 게임 엔진 아키텍처' 카테고리의 다른 글
15. 런타임 게임플레이 기반 시스템 (0) | 2022.08.20 |
---|---|
12. 충돌과 강체 역학 (0) | 2022.08.17 |
11. 애니메이션 시스템 (0) | 2022.08.13 |
10. 렌더링 엔진 (0) | 2022.08.12 |
9. 디버깅과 개발 도구 (0) | 2022.08.10 |
12. 충돌과 강체 역학
12.1 게임에서 물리가 필요한가?
밧줄, 머리카락, 의복 등에 물리적인 동작을 더하면 독특한 게임 효과를 낼 수 있다.
12.1.1 물리 시스템으로 할 수 있는 일
ㄴ 동적 객체와 정적 게임 월드 간의 충돌을 검출
ㄴ 중력이나 기타 힘들의 영향을 받아 자유롭게 움직이는 강체 시뮬레이션 스프링 질량계 (spring-mass-system)
ㄴ 부서지는 빌딩과 구조물
ㄴ 레이 캐스트(ray cast) 와 형상 캐스트(shape cast)
ㄴ 트리거 볼륨(Trigger Volume : 게임 월드의 지정된 지역에 물체가 진입하거나 떠나는 순간을 비롯해 그 안에 자리하고 있는지 등을 판별)
ㄴ 캐릭터가 단단한 물건을 집어 드는 기능
ㄴ 복잡한 기계류 (크레인, 움직이는 플랫폼 퍼즐 등)
ㄴ 래그 돌
ㄴ 움직일 수 있는 소품들과 사실에 근접한 머리카락 및 의복의 움직임
ㄴ 물 표면 시뮬레이션과 부력
ㄴ 오디오 전달
게임에서 런타임에 물리 시뮬레이션을 수행하는 방법 외에도 오프라인 전처리를 통해 애니메이션 클립을 만드는 과정에서도 물리 시뮬레이션을 응용할 수 있다.
12.1.2.1 시뮬레이션 게임
시뮬레이션 게임의 주 목적은 가능한 한 실제와 가까운 경험을 재현하는 것이기 때문에 강체 역학 시스템을 이용해 얻을 수 있는 사실감은 게임에 큰 도움이 된다.
12.1.2.2 물리 퍼즐 게임
물리 퍼즐 게임의 핵심은 게이머가 역학적으로 시뮬레이션되는 장난감들을 이리저리 갖고 놀게 하는 것이다. 물리가 꼭 필요하다.
12.1.2.3 샌드박스 게임
샌드박스 게임에서 플레이어의 주 목적은 이리저리 돌아다니면서 게임 월드의 물건들로 무엇을 할 수 있는지 탐험하는 것이다. 재미를 위해 사실성을 다소 희생하는 경우도 많다. (실제보다 과하게 묘사)
12.1.2.4 목적 중심 게임과 스토리 중심 게임
물리 시스템과는 잘 맞지 않는 편이다.
12.1.3 물리가 게임에 미치는 영향
12.1.3.1 디자인에 미치는 영향
ㄴ 예측 가능성을 저해한다.
ㄴ 튜닝과 컨트롤이 어렵다.
원칙적으로는 게임 디자인을 먼저 고르고 물리 시뮬레이션을 넣을지 말지 정해야 한다.
12.1.3.2 엔지니어링에 미치는 영향
툴 파이프라인
ㄴ 품질이 뛰어난 충돌/물리 파이프라인을 만들고 관리하는데 적잖은 시간이 걸린다.
유저 인터페이스
ㄴ 플레이어가 게임 월드의 물리 객체를 어떻게 컨트롤 할 것인지를 정해야 한다.
충돌 검출 역학
ㄴ 시뮬레이션에 사용될 충돌 모델은 그렇지 않은 모델보다 정교하고 조심해서 만들어야 한다.
AI
ㄴ 물리적으로 시뮬레이션되는 물체들이 섞여 있으면 길 찾기 결과를 정확히 예측하기 힘들 수 있다.
오동작하는 물체
ㄴ 물체들끼리 서로 살짝 뚫고 나갔을 때 의도치 않게 튕겨 나가거나 심하게 요동칠 수 있다.
래그 돌 물리
ㄴ애니메이션에 의해 캐릭터 신체의 일부가 다른 충돌 볼륨을 뚫고 들어갈 수 있는데, 이 상태에서 래그 돌이 되면 매우 불안정한 상태를 야기할 수 있다.
그래픽
ㄴ 물리에 의해 발생하는 움직임은 화면에 그려지는 물체의 경계 볼륨을 변화시킬 수 있다.
ㄴ 미리 계산된 조명이나 그림자를 쓰기 어려울 수 있다.
네트워크와 멀티 플레이어
ㄴ 게임 플레이에 전혀 영향이 없는 물리 효과라면 각 게임 클라이언트에서 따로 계산해도 되지만, 게임 플레이에 영향을 미치는 물리, 예를 들면 수류탄의 궤적 같은 경우는 서버에서 계산한 후, 각 클라이언트에서 정확히 재현해야 한다.
녹화와 재생
ㄴ 역학 시뮬레이션과 녹화 재생 기능을 함께 구현하는 것은 어렵다. 시뮬레이션의 무질서한 특성과 물리 계산의 업데이트 타이밍이 달라질 경우 다시 재생한 장면이 원래 녹화된 것과 달라질 수 있기 때문이다.
12.1.3.3 아트에 미치는 영향
추가적인 도구와 작업 과정에 의한 복잡도 증가
ㄴ 역학 시뮬레이션에 사용될 물체의 질량, 마찰 계수, 제약조건 및 기타 속성들은 아트 팀의 작업을 복잡하게 만든다.
더 복잡한 컨텐츠
ㄴ 외형은 똑같지만 충돌 및 역학 설정이 다른 물체를 여러 개 만들어 다른 목적으로 써야할 수 있다.
ㄴ 예를 들어 원래 형태와 파괴할 수 있는 형태 두 가지를 만들어야 할 수도 있다.
ㄴ 물체 주도 물체는 예측 불가능할 수 있기 때문에 장면의 미적인 구성을 유지하도록 통제하기 힘들 수 있다.
12.1.3.4 기타 영향
ㄴ 엔진지어링, 아트, 기획 부서의 긴밀한 협업이 필요하다. (= 프로젝트의 개발 비용이 증가한다.)
12.2 충돌/물리 미들웨어
12.2.1 I-Collide, SWIFT, V-Collide, RAPID
I-Collide (SWIFT로 대체됨)
ㄴ 노스 캐롤라이나 대학에서 개발한 오픈소스 충돌 검출 라이브러리
ㄴ 볼록 다면체들 간의 교차를 검출할 수 있다.
V-Collide, RAPID
ㄴ 복잡하지만 볼록하지 않은 다면체들도 처리할 수 있는 라이브러리
라이브러리들을 바로 게임 엔진에서 사용할 수는 없지만 게임에 사용할 수 있는 완전한 충돌 시스템을 구현하는데 훌륭한 기반이 될 수 있다.
12.2.2 ODE (Open Dynamics Engine)
오픈소스 충돌 및 강체 역학 SDK (공짜)
모든 소스 코드가 공개되어 있다.
12.2.3 불릿
게임 산업과 영화 산업에서 모두 쓰이는 오픈소스 충돌 검출 및 물리 라이브러리
충돌 엔진과 역학 시뮬레이션은 통합되어 있지만 충돌 시스템만 따로 사용하거나 다른 물리 엔진과 같이 사용할 수 있는 기능을 제공한다.
연속 충돌 검출 방식(Continuous collision detection)을 지원한다.
12.2.4 Vi TrueAxis
비상업적인 용도로는 무료로 쓸 수 있는 충돌/물리 SDK
12.2.5 Physx
비용을 지불하면 모든 소스코드와 라이브러리를 필요에 의해 수정할 수 있다.
12.2.6 하복
오늘날 상용 물리 SDK 중 가장 확고한 위치를 점하고 있으며, 가장 다양한 기능들을 지원하고 모든 플랫폼에서 훌륭한 성능을 자랑한다. (가장 비싸다)
충돌/ 물리 엔진, 차량 물리 시스템, 파괴 가능 환경 모델링 시스템, 래그 돌 물리 시스템과 통합되면서 독립적인 애니메이션 SDK 등을 지원한다.
12.2.7 Physics Abstraction Layer
한 프로젝트 내에서 여러 개의 물리 SDK를 사용할 수 있는 오픈 소스 라이브러리다.
12.2.8 Digital Molecular Matter(DMM)
한정된 방법을 사용해 모양이 변하고 파괴 가능한 물체를 시뮬레이션하는 물리 엔진이다. 오프라인 기능과 런타임 기능을 모두 지원한다.
12.3 충돌 검출 시스템
게임 엔진이 물리 검출 시스템을 사용하는 주된 목적은 게임 월드의 물체들이 서로 접촉을 했느냐를 알아내는 것이다.
이를 위해 검출이 필요한 모든 물체를 하나 이상의 기하적인 형태로 표현해야 한다. 충돌 시스템은 주어진 시간에 이런 형태들이 교차하는지 판별한다.
충돌 시스템은 단순히 교차 여부만 판별하는 것이 아니라 의미 있는 정보도 제공한다. 접촉 정보는 물체가 서로 뚫고 들어가는 부자연스러운 시각 효과를 방지하는데 쓰인다. 보통 다음 프레임을 그리기 전에 뚫고 들어간 물체들을 서로 떨어지게 만드는 방식을 이용한다.
충돌 시스템이 가장 중요하게 사용되는 분야는 강체 역학 시뮬레이션인데, 물체가 튕겨 나가거나 미끄러지거나 멈추는 등 사실적인 물리 현상을 재현하는 용도로 사용된다.
12.3.1 충돌 단위
교차 검사에서는 대개 기하학적으로나 수학적으로 단순한 형상을 선호한다.
바위를 구로 나타낼 수도 있고 자동차의 후드는 사각 박스 형태로 나타낼 수도 있다.
이런 단순한 표현으로 원하는 효과를 얻을 수 없을 때만 더 복잡한 형상을 사용해야 한다.
하복에서 충돌 검출에 사용하는 독립된 강체를 충돌체라고 부른다. (C++ 클래스 khpCollidable의 인스턴스)
PhysX에서는 강체를 액터라고 부른다. (C++ 클래스 NxActor의 인스턴스)
충돌체는 두 가지 중요한 정보를 포함한다. (형상과 변환)
형상은 충돌체의 기하학적인 형태(캡슐, 구, 직육면체)를 나타내고
변환은 게임 월드 안에서의 위치와 방향을 나타낸다.
충돌체에 변환 정보가 필요한 이유
ㄴ 형상은 객체의 생긴 모양을 뜻할 뿐이기 때문에 제대로 쓰이려면 월드 공간에서 제 위치와 방향을 갖게 변환해야한다.
ㄴ 복잡한 형상을 구성하는 요소(여러 개의 점과 평면으로 구성된)을 일일이 움직이려면 느리기 때문
ㄴ 어느 정도 복잡한 형상을 나타내는 데 필요한 정보는 적잖은 메모리를 차지할 수 있는데, 하나 이상의 충돌체가 동일한 형상 정보를 공유할 수 있다면 변환 정보만 다르게 하여 여러 개의 충돌체를 정의 할 수 있다.
게임 객체 중에는 충돌체가 전혀 없는 것도 있고 한 개를 갖고 있는 경우도 있고 여러 충돌체로 이루어진 경우도 있다.
12.3.2 충돌/물리 월드
일반적으로 충돌 시스템은 모든 충돌체를 관리하기 위해 충돌 월드라 불리는 싱글턴 자료 구조를 이용한다.
하복에서 충돌 월드는 hkpWorld 클래스의 인스턴스, PhysX의 충돌 월드는 NxScene의 인스턴스
충돌 월드는 게임의 모든 충돌체를 나타내는 기하학적 입체의 계층 구조에서 루트가 된다.
각 게임 객체 안에 충돌 정보를 넣어 관리하기보다 충돌 정보를 별도의 자료 구조로 유지하면 얻을 수 있는 장점
ㄴ 다른 객체와 충돌할 가능성이 있는 객체의 충돌체만 충돌 월드에 갖고 있으면 된다.
ㄴ 충돌체를 최대한 효율적인 방식으로 조작할 수 있는 여지가 생긴다. (캐시 미스를 최소화)
ㄴ 효과적인 캡슐화 방식으로써 동작한다.
12.3.2.1 물리 월드
게임에 강체 역학 시스템이 있다면 물리 시뮬레이션의 움직이는 강체는 충돌 시스템의 충돌체 하나와 연결된다.
두 SDK에서 강체의 위치와 방향을 고정시킬 수 있는데 이렇게 하면 강체는 역학 시뮬레이션에는 참여하지 않고 충돌체 역할만 한다는 뜻이다.
대부분의 SDK에서는 충돌 라이브러리와 강체 역학 시뮬레이션을 분리하는데 이렇게 하면 물리를 사용하지는 않지만 충돌 검출 기능은 사용하는 게임에서 이점을 얻기 때문이다.
12.3.3 형상의 개념
형상은 경계에 의해 둘러싸이고 분명히 안과 밖이 있는 공간이다. 게임 객체 중 많은 것들이 면으로 표현된다. (지형, 강, 얇은 벽 등) 하지만 면으로는 안과 밖을 표현할 수 없기 때문에 대부분의 충돌 SDK는 면을 기본 단위로 지원하고 형상 개념을 확장해 닫힌 공간과 열린 면을 모두 다룰 수 있게 한다.
일반적으로 충돌 라이브러리는 옵션으로 밀어내기 값을 주어 면이 부피를 갖게 한다. 이 값은 면이 얼마나 두꺼운지를 지정한다. 이렇게 하면 작고 빠르게 움직이는 물체가 극히 얇은 면을 지날 때 충돌을 검출하지 못하는 문제를 줄일 수 있다.
12.3.3.1 교차
기하학적으로 두 형상의 교차는 두 형상 안에 공통으로 들어가는 모든 점(무수히 많은) 이라 할 수 있다.
12.3.3.2 접촉
게임에서는 교차를 찾을 때 엄격한 정의대로 점들의 집합이 필요한 것은 아니다. 단순히 두 물체가 서로 교차하는지 아닌지만 알면 된다.
일반적으로 충돌 시스템은 접촉 정보를 사용하기 편한 자료 구조에 모아 놓고, 접촉이 검출될 때마다 이 자료 구조를 생성한다. 접촉 정보는 다음과 같은 것들을 포함할 수 있다.
ㄴ 분리 벡터 (물체들을 움직여 충돌 상태에서 벗어나게 만들 수 있는 벡터)
ㄴ 접촉이 발생한 두 충돌체에 관한 정보
ㄴ 분리 벡터 방향으로 물체를 움직일 속도
12.3.3.3 볼록함
볼록한 형상은 형상 안에서 시작한 모든 반직선이 표면을 오직 한 번만 통과하는 성질이 있기 때문에 볼록 형상끼리 교차 검사하는 것이 오목 형상이 포함될 때보다 단순하면서 계산도 쉽다.
12.3.4 충돌 기본 단위
12.3.4.1 구
구의 정보는 중심과 반지름으로 이루어져 부동소수 4개로 이루어진 벡터에 넣어 SIMD 수학 라이브러리에 응용할 수 있다.
12.3.4.2 캡슐
원기둥 하나의 양쪽에 반구가 더해진 형태, 구가 지점 A에서 지점 B로 이동하는 동안의 흔적을 나타내는 입체인 스윕구라고 볼 수도 있다.
보통 두 점과 반지름 하나를 사용한다.
원기둥이나 박스보다 교차 검사가 쉽기 때문에 사람의 팔다리와 같이 원기둥과 유사한 형태의 물체를 모델링할 때 캡슐을 많이 사용한다.
12.3.4.3 축 정렬 경계 박스 AABB (axis-aligned bounding box)
AABB는 위치한 좌표계에 상대적인 개념으로만 설명할 수 있다.
AABB는 두 점으로 나타낼 수 있다. (좌표축을 기준으로 박스의 가장 최소 좌표, 최대 좌표)
물체가 박스 모양이고, 물체가 좌표계의 축과 대략적으로 평행일 때에만 잘 동작할 수 있다.
12.3.4.4 유향 경계 박스 (OBB, Oriented Bounding Box)
축 정렬 경계 박스에 좌표축을 기준으로 회전할 수 있게 한 것
너비, 깊이, 높이와 변환(회전, 위치) 정보가 필요하다.
12.3.4.5 DOP (AABB와 OBB를 일반화 한 개념 : Discrete Oriented Polytope)
DOP는 여러 개의 평면을 무한대에 위치시키고, 근사하려는 물체와 만날 때 까지 법선 방향으로 움직여 만든다.
AAOB는 각 평면의 법선이 좌표축에 평행한 성질을 가진 6-DOP
OBB는 각 평면의 법선이 물체의 고유 좌표축에 평행한 성질을 가진 6-DOP
12.3.4.6 임의의 형상을 한 볼록 입체
대부분의 충돌 엔진들은 임의의 형상을 한 볼록 입체도 지원한다. 이런 블록 입체들은 3D 아티스트가 마야 등의 도구를 이용해 만든다. 형상의 삼각형들은 평면의 집합으로 변환되는데, (K-DOP) 이는 평면식 k개로 나타낸다.
12.3.4.7 다각형(폴리곤) 수프
임의의 형태이고 볼록하지 않은 형상을 지원하는 충돌 시스템도 있다. 이 형상들은 대개 삼각형이나 다른 단순한 다각형으로 만든다. 그래서 다각형 혹은 폴리곤 soup라고 불린다.
다각형 수프는 보통 지형이나 빌딩과 같이 복잡하면서 정적인 형상을 모델링할 때 쓰인다.
형상들의 충돌 검출 중 다각형 수프에 대한 충돌 검출이 가장 오래 걸리기 때문에 대부분의 게임에서는 역학 시뮬레이션에 참여하지 않는 물체에만 다각형 수프를 사용하게 된다.
볼록한 다각형이나 단순한 형상들과는 달리 다각형 수프는 닫힌 공간 뿐만 아니라 열린 표면을 나타낼 수 있다.
다각형 수프와 어떤 물체가 충돌 상태에 있을 때 물체를 어느 방향으로 밀어낼지 정하기 위해서 다각형 수프를 이루는 삼각형의 정점 감김 정보를 사용할 수 있다.
12.3.4.8 복합 형상
볼록하지 않은 물체를 모델링할 때 다각형 수프보다 복합 형상이 효율적이 경우가 많다.
어떤 물리 엔진(하복) 에서는 복합 형상 전체의 볼륨 경계 볼륨(하위 형상들을 포함하는 상위 형상) 을 충돌 검사에 이용해 겹치지 않는다면 하위 형상들에 대한 검사를 하지 않는 방법을 사용한다. 이 과정을 중간 단계 층돌 검사라고 부른다.
12.3.5 충돌 검사와 해석 기하학
12.3.5.1 점과 구
점의 중심과 구의 중심의 차이 벡터의 크기를 구의 반지름과 비교
12.3.5.2 구와 구
두 구의 중심의 차이 벡터의 크기와 두 반지름의 합과 비교
12.3.5.3 분할 축 정리
두 볼록 형상을 한 축에 투영했을 때 투영한 이미지들이 겹치지 않는 축이 존재하면, 두 형상은 교차하지 않는다.
어떤 형상은 분할 축이 될만한 후보를 쉽게 찾을 수 있는 특성이 있다
구와 구의 경우
ㄴ 두 구의 중심을 이은 선분과 평행한 축
12.3.5.4 AABB와 AABB
두 AABB가 서로 교차하는지 알기 위해서는 마찬가지로 분할 축 정리를 이용한다.
AABB의 면들은 모두 동일한 좌표축에 평행하다는 성질이 있기 때문에 분할 축이 존재한다면 3 좌표축 중 하나라는 것을 알 수 있다.
각 AABB의 각 좌표축마다 최대 최소 좌표가 모두 겹치는 경우에만 두 AABB가 교차한다고 말할 수 있다.
12.3.5.5 볼록 형상들 간의 충돌 검출: GJK 알고리즘
교차하는 두 블록 형상의 민코프스키 차이는 원점을 포함하지만 교차하지 않는 형상은 그렇지 않다.
GJK 알고리즘은 반복적인 알고리즘으로 처음에는 민코프스키 차이의 껍데기 위에 있는 임의의 점 하나의 단체로 시작하여 점차 차수가 높은 단체를 만들어가면서 그 중에 원점을 포함할 수 있는 것을 찾으려 한다. 지금의 단체보다 원점에 더 가까운 받침 정점 (볼록한 껍데기 위 한 점)을 단체에 더한다. 이렇게 만든 단체가 원점을 둘러싸게 되면 끝난다. (교차함)
12.3.5.6 기타 형상 간의 교차 검사
형상과 형상의 교차를 말할 때 형상 종류가 N개 있으면 교차 검사의 수는 N의 제곱이 되기 때문에 충돌 엔진은 기본 단위 종류의 수를 제한한다.
하지만 GJK는 모든 종류의 블록 형상들 간의 충돌 검출을 한 번에 처리할 수 있다. (알고리즘에서 사용하는 받침 함수만 형상에 따라 다르다.)
하복에서는 충돌 에이전트(hkCollisionAgent 클래스를 상속받는 클래스들)라는 객체들을 이용해 교차 테스트 종류를 결정한다.
충돌 에이전트 클래스에는 (hkpSphereSphereAgent, hkpSphereCapsuleAgent, hkpGskConvexConvexAgent) 등이 있다.
hkpCollisionDispatcher 클래스의 역할은 2차원 디스패치 테이블로 에이전트의 종류를 찾아 충돌체 둘을 에이전트의 인자를 넘겨 호출하는 것이다.
12.3.5.7 움직이는 물체 간의 충돌 검출
작고 빠른 물체는 프레임 사이에 다른 물체를 통과할 수 있는 문제가 있다. (터널링 문제)
해결 방법
1. 스윕 형상
ㄴ 궤적 형상을 이용하는 것 (구 -> 캡슐)
ㄴ 충돌체가 곡선을 그리며 움직이는 경우는 문제가 생긴다.
ㄴ 충돌체가 회전하는 경우에 스윕 형상을 만들면 볼록하지 않을 수 있어 선형 보외법을 사용하여 볼록하게 만들 수 있는데 이는 부정확하다.
2. 연속 충돌 검출 (CCD, Coutinuous Collision Detection)
ㄴ 주어진 시간 구간 내에서 움지깅는 두 물체 간의 가장 이른 충돌 시각을 찾는 것
ㄴ 두 충돌체의 이전 시간 단계(프레임 단위)와 현재 시간 단계의 위치와 방향을 선형 보간해서, 이전 시간 단계와 현재 시간 단계 사이에 있는 임의의 시각에서 충돌체의 변환을 대략적으로 계산할 수 있다.
그런 후 이 움직임 경로에서 가장 이른 TOI (time of impact) 를 검색한다.
12.3.6 성능 최적화
충돌 검출은 CPU를 많이 쓰는 작업이다.
ㄴ 두 형상이 교차하는지 알아내는데 필요한 계산 자체가 매우 복잡하다.
ㄴ 게임 월드에는 수많은 물체들이 있는 것이 보통인데, 교차 검사 횟수는 물체 수가 많아질수록 급격히 증가한다.
물체 n개가 있으면 단순한 방식에서는 모든 쌍을 검사하기 떄문에 알고리즘은 n의 제곱 복잡도를 갖는다.
그렇기 때문에, 공간 해시, 공간 분할, 계층적 경계 볼륨 기법 등이 교차 검사 횟수를 줄이는데 사용된다.
12.3.6.1 시간적 일관성
시간적 일관성 (프레임 간 일관성) 기법은 충돌체가 완만한 속도로 움직이고 있다면 다음 프레임에서 충돌체의 위치와 방향을 예측할 수 있다는 점에서 사용한다.
매 프레임마다 위치와 방향을 다시 계산하는 수고를 덜 수 있다.
하복의 충돌 에이전트(hkpCollisionAgent)는 여러 프레임에 걸쳐 지속적으로 쓰인다.
12.3.6.2 공간 분할
옥트리, 이진 공간 분할 트리, kd-트리 등의 다양한 계층 분할 방식들이 충돌 검출 최적화 과정에서 공간을 분할하는데 쓰이다.
12.3.6.3 넓은 단계, 중간 단계, 좁은 단계
하복에서는 매 시간 단계에서 검사해야 하는 충돌체들을 선별하는 데 세 가지 단계를 사용한다.
1. 대강의 AABB 테스트
2. 복합 형상들을 대강의 경계 볼륨으로 검사
3. 개별적인 충돌 단위들로 테스트
Sweep And Prune 알고리즘
ㄴ 주요 엔진(하복 Physx)들이 모두 넓은 단계 충돌 검출에서 사용하는 알고리즘이다.
ㄴ 충돌체들의 AABB에서 세 좌표축 방향으로 최댓값과 최솟값을 구한 후 정렬된 리스트를 탐색해 교차하는 AABB를 찾는 방식이다. Sweep And Prune 알고리즘은 프레임 간 일관성 기법을 이용해 정렬에 걸리는 복잡도를 O(n log n)에서 O(n)으로 줄일 수 있다.
12.3.7 충돌 질의
캐스트란 임의의 물체를 충돌 월드에 위치시킨 후 반직선 혹은 선분만큼 움직이는 동안 어떤 물체와 충돌하는지, 혹은 충돌하지 않았는지를 판별하는 것이다.
캐스트는 대상이 실제로 충돌 월드에 존재하지 않는다는 점에서 일반적인 충돌 검출 과정과는 다르다.
12.3.7.1 레이 캐스트
시작점과 끝점이 있는 선분을 충돌 월드의 충돌체들과 테스트한다.
선분은 점 하나와 델타 벡터 하나로 표현한다.
충돌 검출 시스템은 시작점에서 가장 가까운 접촉 정보를 리턴하는 경우도 있고 어떤 충돌 검출 시스템은 모든 충돌체들을 알려주기도 한다
12.3.7.2 형상 캐스트
구 캐스트 캡슐 캐스트 등 여러 종류를 사용할 수 있다.
시작점 + 델타 벡터 + 형상의 종류 + 크기 + 방향의 정보를 사용한다.
블록 형상을 캐스트할 때는 다음의 두 경우를 따져봐야 한다.
1. 캐스트할 형상이 다른 충돌체를 관통하거나 아니면 접촉하고 있어 시작점에서 움직일 수 없는 경우
ㄴ 시작 지점에서 충돌이 일어났는지 아닌지 충돌 시스템이 정보를 알려준다. 접촉 지점은 캐스트 형상의 안에 있을 수도 있고, 표면에 있을 수도 있다.
2. 캐스트할 형상이 시작점에서 다른 어떠한 충돌체와 교차하거나 만나지 않아서 움직일 수 있는 경우
ㄴ 다른 물체와 부딪힐 때까지 선분을 따라 이동할 수 있다.
레이 캐스트와 마찬가지로 캐스트 형상의 최초의 접촉들(형상은 한번에 여러 충돌체와 교차할 수 있다) 만 알려주는 API 있는 반면, 가상의 경로를 계속 진행해 그동아느이 모든 접촉을 알려주는 API도 있다.
응용
ㄴ 가상 카메라가 게임 월드의 물체와 충돌하는 지를 알아낼 때 사용한다.
ㄴ 캐릭터의 움직임을 구현할 때, 캡슐 캐스트를 흔히 사용한다. 예를 들면 캐릭터를 평평하지 않은 지형에서 앞으로 밀 떄 구나 캡슐을 캐릭터 발 사이에 놓고 움직이는 방향으로 캐스트하고, 두 번째 캐스트에서 아래위로 움직여보고 지면에 항상 닿아 있게 보정한다.
12.3.7.3 팬텀
게임 월드 안의 특정한 공간(예를 들어 캐릭터의 일정한 반경)에 있는 모든 충돌체를 탐지하고 싶은 경우 사용한다.
이동거리 벡터 D가 0인 구 캐스트와 비슷하다.
구 캐스트와는 달리 팬텀은 충돌 월드에 지속적으로 존재한다. 팬텀은 보이지 않고, 역학 시뮬레이션에 관여하지 않는 충돌체와 같은 의미이다.
12.3.7.4 기타 충돌 질의
하복에는 Closed Point 질의가 있는데, 해당 충돌체에서 가장 가까운 다른 충돌체 위의 점을 찾는데 쓰인다.
12.3.8 충돌 필터링
게임에는 종종 특정한 종류의 물체 사이에 충돌을 켜고 끌 수 있는 기능이 필요하다. 예를 들면 대부분의 물체는 물 표면을 뚫고 지나갈 수 있다. 부력을 시뮬레이션해서 물에 뜨게 만들거나 바닥에 가라앉게 할 수도 있지만 어쨌건 수면이 단단해서는 안 된다. 거의 모든 충돌 엔진에서는 게임 나름의 기준에 따라 충돌체 간의 접촉을 허용하거나 허용하지 않을 수 있는 기능이 있다. 이를 충돌 필터링이라고 한다.
12.3.8.1 충돌 마스킹과 계층
게임 월드의 물체들을 범주로 나눈 후 테이블을 이용해 특정 범주의 물체가 다른 물체와 충돌할 수 있는지 없는지를 찾는 방법이다.
12.3.8.2 충돌 콜백
충돌이 검출될 때마다 콜백 함수를 호출하는 방법
하복 기준
접촉 지점이 처음 월드에 더해지면 contactPointAdded() 콜백을 호출
접촉 지점이 이후에 올바른 것으로 판별되면(이보다 이른 TOI가 발견되지 않으면) contactPointConfirmed() 콜백을 호출
이 콜백 함수들 안에서 충돌하는 대상에 따라 접촉 지점을 거부할 수 있다(필터링 할 수 있다).
12.3.8.3 충돌 머터리얼
충돌 했을 때 나는 소리나 효과 등의 부가적인 효과를 제어하기 위해 게임 월드의 충돌체들을 분류해야 할 필요가 있다.
충돌 기본 단위와 충돌 머터리얼을 연결하는데 흔히 쓰이는 방법은 8, 16, 32 비트 정수를 사용하는 것인데, 이 값으로 상세한 충돌 속성이 저장된 자료구조 배열에 접근한다.
12.4 강체 역학
게임 물리 엔진에서는 게임 월드 안에 있는 강체들의 움직임에 대해 여러 가지 제약 조건을 줄 수 있다.
가장 널리 쓰이는 제약 조건은 통과할 수 없는 조건이다.
대부분의 물리 시스템들은 시뮬레이션 하는 강체 간에 사실적인 상호작용 구현을 위해 개발자가 별도의 제약조건을 설정할 수 있게 지원하기도 한다.
경첩, 직선축 관절, 볼 관절, 바퀴, 래그 돌 등이 있다.
역학 시뮬레이션의 강체와 충돌 엔진의 충돌체 간에는 대개 일대일 관계가 있다. 하복의 hkpRigidBody 객체는 오직 한 개의 hkpCollidable 참조만 갖고 있다.
12.4.1 기본 지식
12.4.1.1 단위
강체 역학 시뮬레이션은 대부분 MKS 단위계를 이용한다. 미터(m), 킬로그램(kg), 초(s)를 사용한다.
12.4.1.2 선 동역학과 각 동역학의 구분
제약 없는 강체는 직교 좌표계의 세 축 방향으로 평행 이동하면서 마찬가지로 세 축에 대해 자유롭게 회전할 수 있는 물체다. 이런 경우 물체는 6자유도를 갖는다고 한다.
제약 없는 강체의 운동은 다음과 같이 서로 완전히 독립된 두 부분으로 나눌 수 있다.
선 동역학
ㄴ 회전 성분을 무시할 때 물체의 운동
각 동역학
ㄴ 물체의 회전 운동을 나타낸다.
질량 중심
ㄴ 선 동역학의 관점에서 제약 없는 강체는 모든 질량이 질량 중심이라 불리는 한 점에 있는 것처럼 행동한다.
ㄴ 강체의 질량은 질량 중심에서 모든 방향으로 균등하게 배분된다.
ㄴ 밀도가 균일한 물체의 경우 질량 중심은 입체의 기하 중심과 같다.
12.4.2 선 동역학
선 동역학의 관점에서는 어떤 강체의 위치를 위치 벡터로 표현할 수 있는데, 이 벡터는 월드 공간 원점에서 그 강체의 질량 중심을 가리킨다.
12.4.2.1 선속도와 선 가속도
강체의 선속도란 이 물체의 질량 중심이 움직이는 속도와 방향으로 결정된다. 벡터로 표현한다.
벡터 값으로 일반적으로 초당 미터 (m/s) 단위로 나타낸다.
시간에 대한 위치의 일차 도함수는 속도, 이차 도함수는 가속도이다.
12.4.2.2 힘과 운동량
질량이 있는 물체를 가속하거나 감속하는 모든 것을 힘이라고 한다.
크기와 뱡향이 있기 때문에 벡터 값(F)으로 표현한다.
F = ma 이므로 힘은 kg-m/s² 단위(뉴턴)이다.
12.4.3 운동 방정식 풀기
강체 역학의 핵심 문제는 작용하는 힘이 주어졌을 때 물체의 움직임을 구하는 것이다.
선 동역학의 경우 순수 힘 F(t)나 이전 시간의 물체 위치와 속도 등의 정보를 가지고 v(t)와 r(t)를 찾는 것을 의미한다.
12.4.3.1 함수로서의 힘
힘은 상수인 경우도 있지만 시간에 대한 함수인 경우도 있다.
또 힘은 물체의 위치나 속도 등 다른 값에 대한 함수가 될 수도 있다.
12.4.3.2 상 미분 방정식 (ODE, Ordinary Differential Equation)
상 미분 방정식이란 하나의 독립 변수 (t)에 대한 함수 하나와 그 도함수들로 이루어진 방정식을 일컨는 말이다.
12.4.3.3 분석적 해법
중력과 같이 가속도가 고정인 경우(힘이 일정한 경우) 에는 가능하지만 게임 물리에서는 분석적인 해법을 쓸 수 있는 경우가 드물다. 게임은 상호적인 시뮬레이션이기 때문에 힘이 어떻게 변할지 예측할 수 없다.
12.4.4 수치 적분
현재 시각에서 물체의 위치와 속도를 알고, 힘이 시간, 위치, 속도의 함수라면 이걸들을 이용해 다음 시각에서의 물체의 위치와 속도를 찾는 것이 목표이다.
12.4.4.1 명시적 오일러 법
모든 시간 단계마다 물체의 속도가 상수라는 가정을 하여 현재의 속도를 사용하여 다음 프레임에서 물체의 위치를 예측한다.
시간에 따라 속도가 일정할 때만 정확하다.
12.4.4.2 수치 해석적 방법의 특성
상 미분 방정식(ODE)에 대한 수치 해석적인 방법은 다음과 같은 속성이 있다.
수렴성
ㄴ 시간 단계 t가 0으로 수렴함에 따라 근사적인 해법은 점점 진짜 해법에 가까워지는가?
차수
ㄴ 오차는 대새 시간 단계 Δt의 몇 제곱에 비례하는가? 오차가 O(Δt²)라면 차수 1 이라고 한다.
ㄴ 명시적 오일러 법은 차수가 1인 방식이다. (테일러 급수에서 Δt의 1 제곱인 항까지 정확하기 때문)
ㄴ 차수가 클수록 오차가 작아진다.
ㄴ 오차는 대개 ODE를 테일러 급수 전개한 것과 ODE를 빼서 남아있는 항들의 차수를 이용해 구한다.
안정성
ㄴ수치 해석적인 해법이 시간이 지남에 따라 안정화되는가?
12.4.4.3 명시적 오일러 법 이외의 방식
명시적 오일러 법은 단순한 구현에 많이 쓰이며, 속도가 일정한 경우에는 잘 적용될 수 있다.
범용 역학 시뮬레이션에서는 오차가 크고 안정성이 낮기 때문에 쓰지 못한다.
여러 수치 해석적인 방법들이 ODE를 푸는데 쓰인다.
ㄴ 후향 오일러 법 (차수가 1인 방식)
ㄴ 중점 오일러 법 (차수가 2인 방식)
ㄴ 여러 종류의 Runge-Jutta 방식
12.4.4.4 베를레 적분
오늘날 상호적인 게임에서 가장 많이 쓰이는 수치 해석적 ODE 해법이다.
일반 베를레 방식과 속도 베를레 방식이 있다.
일반 베를레 방식
ㄴ 오류 차수가 높다.(오차가 작음)
ㄴ 계산하기 복잡하지 않다.
ㄴ 가속도가 주어졌을 때 한 번에 위치를 바로 구할 수 있다.
일계 도함수 항(속도)이 사라짐을 알 수 있다.
순수 힘에 관한 식으로 베를레 방식을 표현하면 다음과 같다.
속도는 여러가지 방법으로 근사치를 얻을 수는 있다.
12.4.4.5 속도 베를레 방식
속도 베를레 방식은 일반 베를레 방식보다 널리 쓰인다. 네 단계로 이루어져 있고 편의를 위해 시간 단계를 두 부분으로 나눈다.
12.4.5 2차원 각 동역학
제약 없는 강체는 질량의 중심을 중심으로 회전 운동을 한다.
이 말은 물체 질량 중심의 선운동에 각운동을 더하면 물체의 모든 운동을 완전히 나타낼 수 있다는 뜻이다.
이처럼 물체에 힘이 가해졌을 때 회전 운동을 연구하는 분야를 각동역학(angular dynamics) 이라고 한다.
2차원에서는 각 동역학은 선 동역학과 완전히 똑같은 식으로 동작한다.
12.4.5.1 방향과 각속력
2차원에서는 모든 선 운동이 xy 평면에서만 일어나고 모든 회전 운동은 z축을 기준으로 발생한다.
2차원 강체의 방향은 각 θ만 갖고도 나타낼 수 있다. x축 양의 방향을 바라보고 있을 때를 θ = 0으로 정할 수 있다.
12.4.5.2 각속력과 가속도
각속도란 물체의 회전각이 시간에 따라 얼마나 빠르게 변하는지를 나타내는 값이다
각속도는 스칼라 함수 ω(t)로 나타내고, 단위는 초당 라디안 (rad / s)이다.
각가속도 α(t)는 각속도의 변화율이며, 초의 제곱당 라디안 (rad / s²)이다.
12.4.5.3 관성 모멘트
점 질량의 선 속도를 바꾸기 힘든 정도를 나타내는 개념이 질량이라면, 어떤 강체를 특정한 축 기준으로 회전할 때 각속도를 바꾸기 힘든 정도를 나타내는 개념이 관성 모멘트이다.
물체의 질량이 회전축에 모여있는 경우에는 상대적으로 회전하기 쉽기 때문에 회전축에서 멀리 떨어져 있는 경우보다 관성 모멘트가 작다.
지금은 2차원 각운동만을 다루기 때문에 회전축은 항상 z축이며, 물체의 관성 모멘트는 그냥 스칼라 값이다. 관성 모멘트는 흔히 기호 I로 나타낸다.
12.4.5.4 토크
힘이 작용하는 경로가 물체의 질량 중심을 통과하는 경우는 지금까지 봐 왔던 선 운동만 일어난다. 그렇지 않은 경우 선 운동 외에도 토크라 불리는 회전하는 힘이 생긴다.
물체의 질량 중심에서 힘이 작용하는 지점까지의 벡터 r과 힘 F를 외적 연산 한 값이 토크 N이다
N = r X F
2차원에서 r과 F는 모두 xy 평면에 놓여있기 때문에 N은 언제나 z축 방향이다.
12.4.6 3차원의 각 동역학
12.4.6.1 관성 텐서
3차원에서 강체의 회전 질량은 관성 텐서라 불리는 3x3 행렬로 나타낸다. 이것은 보통 기호 I로 표기한다.
이 행렬의 대각선 성분들은 좌표계의 세 축에 대한 관성 모멘트를 나타낸다. 대각선이 아닌 성분들은 관성 곱이라고 부른다. 이 값들은 물체가 세 좌표축에 모두 대칭일 때는 0이 된다.(육면체 박스처럼) 이 값들이 0이 아닐 경우 물리적으로는 사실적이지만 다소 직관적이지 않아 보이는 운동을 하게 되는데, 보통 게이머들은 이것을 사실적이지 않은 운동이라고 생각하기 쉽다. 그렇기 때문에 게임 엔진에서 쓰이는 관성 텐서는 종종 성분이 세 개인 벡터로 단순화하는 경우가 많다.
12.4.6.2 3차원에서의 방향
사원수를 물체의 방향을 나타내는 데 사용하고, 물체의 방향은 시간에 대한 함수이므로 q(t)로 써야 한다.
회전각이 0인 임의의 방향을 정해야 한다.
12.4.6.3 3차원에서의 각속도와 운동량
3차원에서 각속도는 벡터 값이고 ω(t)로 표기한다.
각속도 벡터는 회전축을 나타내는 단위 벡터 u에 그 축에 대한 물체의 2차원 각속도를 곱한 것으로 생각하면 된다.
3차원에서는 회전하는 강체에 작용하는 힘이 없더라도 각속도 벡터 ω(t)는 일정하지 않을 수 있는데, 이것은 회전축이 계속 변할 수 있기 때문이다.
각 속도는 보존되는 값이 아니기 때문에 역학 시뮬레이션에는 각 운동량 L을 주요 물리량으로 다룬다.
각 운동량 L(t) = Iω(t)
12.4.6.4 3차원에서의 토크
3차원에서도 토크를 구하는 방법은 힘의 작용점 벡터와 힘 벡터의 외적을 사용한다.
각속도는 보존되는 값이 아니기 때문에 각운동량을 기준으로 표현해야 한다.
운동량 = 질량 x 속도
각운동량 = 관성 모멘트 x 각속도
12.4.6.5 3차원 각운동 방정식 풀기
어려우니까 결론만
12.4.7 충돌 응답
물체가 서로 충돌하는 경우 역학 시뮬레이션에서는 물체드링 사실적으로 충돌에 반응하면서 시뮬레이션 단계가 끝나는 시점에서는 서로 뚫고 들어간 상태로 남아 있지 않게 조치를 취해야 한다. 이를 충돌 응답이라고 한다.
12.4.7.1 에너지
힘은 강체들의 계(system) 에 에너지를 더하거나 (폭발) 에너지를 감소시킨다. (마찰)
에너지는 위치 에너지와 운동 에너지로 구성된다.
운동 에너지
12.4.7.2 충격적 충돌 응답
대부분의 실시간 시뮬레이션들은 뉴턴의 법칙에 기반을 두고 운동량과 운동에너지를 분석한다.
이 법칙은 충돌의 속성을 단순화하는 다음과 같은 가정에 바탕을 둔다.
1. 충돌 힘은 무한히 짧은 시간에 작용하고, 따라서 이상적인 충격이라고 할 수 있다. 그렇기 때문에 물체의 속력은 충돌 결과 즉시 변한다.
2. 물체들이 접촉하는 지점의 표면에는 마찰이 없다. 이 말은 충돌 과정에서 두 물체를 떨어뜨리려는 충격은 접촉 표면에 수직으로 작용한다는 뜻이다. 즉 충돌 충격에는 비스듬한 성분이 없다.
3. 충돌 과정에서 일어나는 물체들의 분자 단위 반응들은 반발 계수(보통 ε로 표현한다.)라는 수 하나로 흉내 낼 수 있다. 이 수는 충돌로 인해 얼마만큼의 에너지를 잃는가를 나타낸다. ε = 1일 경우 완전 탄성 충돌이고 에너지를 잃지 않는다. ε = 0일 경우에는 비탄성 충돌이며 두 물체는 운동에너지를 완전히 잃는다.
12.4.7.3 페널티 힘
충돌 응답을 구현하는 또 다른 방식에는 실제로는 존재하지 않는 페널티 힘이라는 개념을 시뮬레이션에 도입하는 방법이 있다.
스프링 상수 K는 교차해 있는 시간을 효율적으로 조정하는 역할을 하고, 감쇠 계수 b는 반발 계수와 다소 비슷한 역할을 한다.
b = 0인 경우 감쇠 효과는 없다. (완전 탄성 충돌이다)
12.4.7.4 제약조건을 통한 충돌 해결
대부분의 물리 엔진에서 물체의 운동에 여러 제약 조건을 넣어 시뮬레이션 할 수 있다.
제약조건으로 충돌을 처리한다면 그냥 시뮬레이션의 제약조건 해결사(constraint solver)를 실행해 해결하면 된다. 제약조건 해결사가 빠르고 품질 높은 시각적 결과를 뽑아낸다면 이 또한 효율적인 충돌 처리법이 된다.
12.4.7.5 마찰
12.4.7.6 용접 (WELDING)
물체가 다각형 수프를 따라 미끄러지고 있는 경우 추가적으로 문제가 발생한다.
물체가 한 삼각형에서 같은 다각형 수프의 다른 삼각형으로 미끄러지는 경우 충돌 검출 시스템은 이 물체가 다음 삼각형의 모서리에 닿을 것을 예측해 불필요한 충돌을 감지하게 된다.
하복 4.5에서는 메시에 삼각형 인접 정보를 첨가하는 방식을 사용한다.
이 방식을 사용하면 충돌 검출 시스템은 어떤 모서리가 어느 모서리가 다각형 수프 내에 있는 모서리인지 알수 있기 때문에 가짜 충돌 정보를 정확하고 빠르게 버릴 수 있다. 하복은 이것을 용접이라고 부르는데, 사실상 다각형 수프의 삼각형들의 모서리를 용접한 것과 마찬가지이기 때문이다.
12.4.7.7 점차 멈춰 서기, 섬, 잠재우기
운동하던 물체가 마찰, 감쇠 등을 통해 에너지를 잃으면 움직이던 물체들은 점차 멈춰야 한다. 하지만 컴퓨터 시뮬레이션을 통해 점차 멈춰서는 일은 부동소수 오차나 반발력 계산 오류, 수치적인 불안정성 등의 여러 요인 때문에 물체들이 자연스럽게 멈춰 서지 못하고 계속 불안정하게 떨리게 된다.
시스템에서 물체의 에너지를 제거할 수도 있고, 물체의 속도가 일정한 기준 아래로 내려가면 그냥 속도를 0으로 만들 수도 있다.
물체가 움직이지 않게 되면 더 이상 매 프레임 물체의 운동 방정식을 계산할 필요가 없기 때문에 매 프레임 계산 대상에서 제외할 수 있다. (잠재우기) 잠자는 물체들은 시뮬레이션에서는 잠시 제외되지만 충돌에서는 여전히 유효하며, 잠자는 물체에 힘이나 충격이 작용하거나 물체를 평행 상태로 지탱하고 있던 접촉 지점을 잃게 되면 깨어나게 되고 다시 동적 시뮬레이션을 시작한다.
잠재우는 기준(평행 상태 판단 기준)
ㄴ 물체가 3개 이상의 점촉 지점(한 개 이상의 접촉 평면)을 통해 중력 등 기타 작용하는 힘과 평형을 이루고 있는 상태
ㄴ 물체의 선 운동량과 각 운동량이 미리 정해진 기준보다 낮을 때
ㄴ 물체의 선 운동량과 각 운동량의 이동 평균이 정해진 기준보다 낮을 때
ㄴ 물체의 총 운동 에너지 ( T = 1/2p v + 1/2L ω)가 정해진 기준보다 낮을 때
시뮬레이션 섬
하복과 PhysX에서는 한층 더 성능을 최적화하기 위해 서로 영향을 주거나 곧 영향 줄 가능성이 있는 물체들을 자동으로 한데 묶어 시뮬레이션 섬이라고 지칭한다.
각 시뮬레이션 섬들은 서로 독립적으로 시뮬레이션되기 때문에 캐시 일관성이나 병렬 처리에 유리한 방식이다.
잠재우기를 시뮬레이션 섬 단위로 한다.
12.4.8 제약조건
제약 없는 강체는 세 방향으로 평행 이동할 수 있고, 세 직교 좌표축을 기준으로 회전할 수 있어서 자유도가 6이다.
제약 조건은 강체의 자유도를 부분적으로 감소시키거나 없앨 수 있다.
물리 SDK에서는 다양한 제약조건을 지원한다.
12.4.8.1 점과 점 제약조건
물체 A의 한 점이 물체 B의 한 점에 항상 붙어 있어야 한다.
12.4.8.2 단단한 스프링
물체 A의 한 점은 물체 B의 한 점과 일정한 거리만큼 떨어져 있어야 한다.
12.4.8.3 경첩 제약조건
회전 운동의 자유도를 1로 제약해 경첩의 축 방향으로만 회전할 수 있도록 한다.
12.4.8.4 각기둥 제약조건
평행 운동의 자유도를 1로 제약해 축 방향으로만 평행이동 할 수 있도록 한다.
12.4.8.5 흔히 쓰이는 다른 제약조건
평면, 바퀴, 도르래
12.4.8.6 제약조건 체인
여러 물체가 제약조건들로 인해 체인 형태로 연결된 경우 시뮬레이션하기 어려울 때가 있는데, 제약 조건 체인이라는 제약조건들의 특수한 그룹을 사용하여 제약 조건 해결사에 물체들이 어떻게 연결되어 있는지에 대한 정보를 제공해야 한다.
12.4.8.7 래그 돌
제약조건 체인을 이용한다.
래그 돌은 물리 시스템에 의해 좌우되는 (물리월드의 강체들의 위치와 회전으로 애니메이션 뼈대의 위치와 방향을 정한다.) 절차적 애니메이션 (실행 시간에 생성하는 애니메이션)이다.
12.4.9 강체의 움직임 제어
대부분의 게임에서는 디자인적 요소로 중력의 영향을 받아 움직이거나 여러 물체들 사이의 충돌에 의해 자연스럽게 운동하는 강체들의 움직임을 어느 정도 통제할 수 있어야 한다.
12.4.9.1 중력
중력은 힘이 아니라 가속도이기 때문에 질량에 관계없이 모든 물체에 똑같은 영향을 미친다.
모든 곳에서 쓰이면서 특수한 성질이 있기 때문에 SDK의 전역 설정으로 지정하는 경우가 많다.
12.4.9.2 힘 가하기
게임에서 물리 시뮬레이션을 하는 동안 물체들에는 수많은 힘이 가해질 수 있다. 힘은 한정된 시간에 걸쳐 작용한다.(즉시 작용하는 경우 충격이라고 한다.)
게임 안에서 힘은 특성상 대개 동적이다.(프레임마다 바뀐다.) 그렇기 때문에 거의 모든 물리 SDK에서는 힘을 적용하는 함수들을 프레임마다 한 번씩, 힘이 유효한 동안(여러 프레임) 계속 호출하도록 설계한다.
12.4.9.3 토크 가하기
질량 중심에서 등거리인 두 지점에 크기가 같고 방향이 정반대인 힘을 가하면 순수하게 토크만 생긴다.
이처럼 쌍으로 작용해 토크를 만드는 두 힘을 짝힘이라고 한다.
12.4.9.4 충격 가하기
충격은 즉각적인 운동량의 변화다.
엄밀한 의미에서 충격은 무한히 짧은 시간동안 작용하는 힘이다. 시간 간격을 통해 역학 시뮬레이션을 구현하는 경우 가장 짧은 힘 적용 시간은 Δt인데 충격을 제대로 흉내내기에는 너무 긴 시간이다. 대개의 물리 엔진은 applyImpulse 같은 함수로 물체에 충격을 가하게 된다.
12.4.10 충돌/물리 단계
충돌/물리 엔진의 업데이트 단계에서 일어나는 일
1. 물리 월드의 물체들에 힘과 토크를 Δt 만큼 앞선 시각으로 계산하여 다음 프레임에서의 위치와 방향을 시험적으로 결정한다.
2. 시험적인 이동으로 인해 물체들 간에 새로운 접촉 정보가 생겼는지를 알아내기 위해 충돌 검출 라이브러리를 호출한다.(시간적 일관성을 활용하기 위해 물체들은 각각의 접촉 정보를 유지한다. 따라서 시뮬레이션하는 매 단계 충돌 엔진은 기존의 접촉 정보가 유효하지 않게 바뀌었느지와 새로운 접촉 정보가 더해졌는지만 알아내면 된다)
3. 충돌을 해결한다. 충격을 가하거나 페널티 포스를 가하는 방법, 또는 제약조건 해결 과정에서 처리하는 방법 등이 쓰인다. SDK에 따라서는 이 단계에 충돌 검출을 수행할 수도 있다.
4. 제약조건 해결사에 의해 제약조건을 충족시키게 한다.
12.4.10.1 제약조건 해결사
제약조건 해결사는 반복적 알고리즘이다.
물체의 실제 위치 및 회전을 제약조건이 충족될 떄의 이상적인 위치 및 회전과 최대한 근접하게 함으로써 다수의 제약조건을 동시에 만족시키는 것이 목적이다. 따라서 제약 조건 해결사는 반복적인 오차 최소화 알고리즘이라 할 수 있다.
물리 시뮬레이션의 매 단계에서는 수치 적분을 이용해 물체들의 변환을 예측한다. 그런 후 제약조건 해결사는 물체들의 상대적인 위치를 가지고 두 물체가 공유하는 회전축이 얼마나 틀어졌는지를 계산한다. 오차가 발견되면 해결사는 물체들을 움직여 오차를 최소화하거나 아니면 없애려 시도한다.
오차가 없다면 더 이상 반복할 필요가 없다.
여러 개의 제약조건을 동시에 충족해야 하는 경우에는 여러 번 반복해야 할 수도 있다.
오차를 최소화할 수 있게 잘 설계된 제약조건 해결사가 있지만 그렇지 않은 경우에는 물체들이 엉뚱하게 움직이는 것을 볼 수도 있다.
12.4.10.2 엔진 간의 구현 차이
엔진마다 여러 계산 단계를 수행하는 방법이나 상대적인 수행 순서는 물리 SDK마다 차이가 있을 수 있다.
12.5 물리 엔진과 게임 통합
12.5.1 게임 객체와 강체간의 연결
게임 객체는 충돌/물리 월드에 여러 개의 강체로 표현되기도 하고 강체가 아예 없을 수도 있다.
1. 강체가 없는 경우
ㄴ 장식으로 쓰이는 물체들이나, 볼 수는 있지만 근처에 갈 수 없는 물체로 쓰인다.
2. 강체가 하나인 경우
ㄴ 대부분의 단순한 게임 객체는 강체 하나로 표현할 수 있다. 이 경우 강체의 충돌체는 가능한 한 게임 객체의 모양과 비슷한 것을 고른다.
3. 강체가 여러 개인 경우
ㄴ 캐릭터, 탈것 등 여러 견고한 부분들로 구성된 물체들. 이런 게임 객체들은 대개 뼈대를 이용해 각 부분의 위치를 관리한다.
게임 객체와 강체 간의 연결은 엔진에서 관리하며, 보통 각 게임 객체가 자신의 강체들을 관리하는 경우가 많다.
12.5.1.1 물리 주도 물체
게임에 강체 역학 시스템을 사용하는 경우 완전히 시뮬레이션에 의해서만 움직이는 물체들을 물리 주도 물체들이라고 한다.
우선 시뮬레이션을 처리하고 그후 물리 시스템에서 물체의 위치와 방향을 얻어 온다. 이 변환 정보는 게임 객체에 그대로 적용되거나 아니면 일부 관절, 혹은 그 안의 자료 구조 등에 적용된다.
12.5.1.2 게임 주도 물체
게임 물체들 중 일부는 물리에 영향 받지 않고 움직여야 한다. 이런 물체들은 충돌 검출에는 참여해야 하지만(에를 들면 물리 주도 물체들을 밀쳐내며 이동하는 경우), 물리 엔진에 의해 움직임에 영향을 받아서는 안 된다. 이런 기능을 위해 대부분의 물리 SDK는 게임 주도 물체라 불리는 특수한 강체를 지원한다. (하복에서는 key framed body라고 부른다).
게임 주도 물체들은 시뮬레이션 하는 동안 힘과 충돌 충격에 의해서 속도가 바뀌지 않아야(움직이지 않아야) 하기 때문에 무한대의 질량을 가진다고 설정한다.
게임 주도 물체들을 물리 월드에서 움직이렴녀 단순히 매 프레임마다 게임 객체의 자리를 따라 위치와 방향을 바꿔서는 안 된다. 이렇게 하면 물리 시뮬레이션에서 해결하기 굉장히 어려운 불연속적인 요소를 집어넣게 된다.
그렇기 때문에 게임 주도 물체들은 대개 충격을 이용해 다음 프레임에 물체가 원하는 위치에 오게 속도에 즉각적인 변화를 준다. 대부분의 물리 SDK는 다음 프레임에서 원하는 위치와 방향에 도달하는데 필요한 충격의 성분과 각 성분을 계산해주는 함수를 제공한다.
12.5.1.3 고정된 물체
게임 월드는 대개 정적인 형상과 동적인 객체들이 섞여 이루어진다. 게임 월드의 정적인 부분을 모델링하기 위해 대부분의 물리 SDK에는 고정된 물체라 불리는 특수한 종류의 강체가 있다. 고정된 물체는 게임 주도 물체와 비슷한 면이 있지만 동적인 시뮬레이션에는 전혀 관여하지 않는다. 이것들은 충돌만 하는 물체들이다.
12.5.1.4 하복의 모션 타입
하복에서는 hkpRigidBody 클래스의 인스턴스를 이용해 모든 종류의 강체들을 나타낸다. 각 인스턴스에는 모션 타입을 지정하는 부분이 있다. 모션 타입은 물체가 고정돼 있는지, 아니면 게임 주도인지 물리 주도인지를 시스템에 알려주는 역할을 한다.
하복은 물체의 종류를 모션타입만을 바꿈으로써 쉽게 바꿀 수 있다.(고정된 물체, 게임 주도 물체, 물리 주도 물체)
12.5.2 시뮬레이션 업데이트
물체 시뮬레이션을 완전히 업데이트 하려면 다음과 같은 단계를 거쳐야 한다. 업데이트는 대개 프레임당 한 번씩 한다.
게임 주도 강체 업데이트
ㄴ 물리 월드에 있는 게임 주도 강체들의 변환을 업데이트해서 게임 월드에 있는 쌍들의 변환과 일치되게 한다.
팬텀 업데이트
ㄴ 팬텀 형상은 게임 주도 충돌체처럼 동작하지만 강체는 없다. 이는 여러 종류의 충돌 질의를 하는 데 쓰인다. 팬텀의 위치는 물리 단계 전에 업데이트해 충돌 검출이 수행될 때 제 위치에 자리하게 한다.
힘 업데이트, 충격 적용, 제약조건 조정
ㄴ 게임에서 가해지는 모든 힘들을 적용한다.
ㄴ 해당 프레임에서 게임 이벤트에 의해 발생한 모든 충격을 적용한다.
ㄴ 필요한 경우 제약조건을 조정한다.
시뮬레이션 단계 밟기
ㄴ 운동 방정식을 수치 적분하는 과정
ㄴ 충돌 검출
ㄴ 충돌 해결
ㄴ 제약조건 적용하기
물리 주도 게임 객체 업데이트
ㄴ 물리 월드에서 물체들의 변환을 뽑아 낸 후 해당하는 게임 객체나 관절의 변환을 업데이트한다.
팬텀 질의
ㄴ 물리 단계가 끝난 후, 각 팬텀 형상의 접촉 정보를 읽어와 다양한 의사 결정에 이용한다.
충돌 캐스트 질의 수행
ㄴ 레이 캐스트와 형상 캐스트를 수행하는데, 이 과정은 동기화되는 경우도 있고 비동기화 되는 경우도 있다. 얻은 결과는 게임 엔진에서 다양한 의사 결정에 쓰인다.
업데이트 의존 관계
ㄴ 게임 주도 물체 업데이트와 힘/충격 업데이트는 시뮬레이션 단계보다 앞에 수행해야 한다.
ㄴ 물리 주도 게임 객체 업데이트는 시뮬레이션 단계가 끝난 후에 수행되어야 한다.
ㄴ 레이 캐스트와 형상 캐스트는 게임 루프 안에서 아무 때나 할 수 있다.
ㄴ 렌더링은 게임 루프의 마지막에 온다.
12.5.2.1 충돌 질의 시점 정하기
충돌 시스템에서 최신 정보를 얻어 오려면 해당 프레임의 물리 단계(시뮬레이션 단계)가 끝난 후 충돌 질의를 던져야 한다. 하지만 물리 단계는 대개 프레임의 맨 마지막에 수행되는데 이때는 이미 게임 로직에서 대부분의 의사 결정이 끝난 상태이고 게임 주도 물체들의 새 위치도 정해진 상태이다.
충돌 질의를 언제 던져야 하는지에 대한 정답은 없지만 2가지 선택 방법이 있다.
1. 이전 프레임의 상태를 바탕으로 정하기
ㄴ 이전 프레임의 충돌 정보를 바탕으로 해도 정확한 의사 결정을 할 수 있는 경우가 많기 때문에 사용 가능하다.
ㄴ 물체가 너무 빠르게 이동하면 오류가 생길 수 있다.
2. 물리 단계 이후에 충돌 질의하기
ㄴ 충돌 질의 결과를 바탕으로 한 의사 결정이 맨 마지막까지 미뤄져도 상관 없는 경우 이 방법을 쓸 수 있다. 충돌 질의에 따라 달라지는 렌더링 효과가 그 좋은 예다.
12.5.3 게임에서 충돌과 물리를 사용하는 예
나중에 필요할 때 찾아보기
12.5.3.1 단순한 강체를 갖는 게임 객체
12.5.3.2 총알 추적
12.5.3.3 수류탄
12.5.3.4 폭발
12.5.3.5 파괴 가능 물체
12.5.3.6 캐릭터 구현
12.5.3.7 카메라 충돌
12.5.3.8 래그 돌 통합
12.6 고급 물리 기능
강체 시뮬레이션을 넘어서 물리엔진이 확장되는 분야들
가변 형상 물체
ㄴ 하드웨어가 발달하고 고성능 알고리즘이 개발되면서 물리 엔진에서도 가변 형상 물체를 지원하기 시작했다.
의복
ㄴ 의복은 스프링으로 연결된 점 질량들이 종이 형태로 배열된 모양으로 모델링할 수 있다. 하자만 의복은 다른 물체와의 충돌이나 시뮬레이션에서 수치적인 안정성 등 많은 부분에서 문제가 발생할 수 있기 때문에 구현하기 까다롭다.
머리카락
ㄴ 수많은 가는 섬유 형태로 모델링하거나 밧줄 형태나 가변 형상 물체로 모델링할 수 있다.
물 표면 시뮬레이션과 부력
일반 유체 역학 시뮬레이션
물리 기반 오디오 합성
ㄴ 물리적으로 시뮬레이션되는 물체가 좀 더 사실적으로 보이게 하기 위해서 적절한 오디오를 동적으로 합성해 생성하는 방법
GPGPU
ㄴ GPU의 병렬 처리 능력을 그래픽이 아닌 다른 일에 사용
'읽은 책 > 게임 엔진 아키텍처' 카테고리의 다른 글
15. 런타임 게임플레이 기반 시스템 (0) | 2022.08.20 |
---|---|
14. 게임플레이 시스템의 소개 (0) | 2022.08.17 |
11. 애니메이션 시스템 (0) | 2022.08.13 |
10. 렌더링 엔진 (0) | 2022.08.12 |
9. 디버깅과 개발 도구 (0) | 2022.08.10 |
11. 애니메이션 시스템
11.1 캐릭터 애니메이션의 종류
11.1.1 셀 애니메이션
셀 애니메이션을 전자적인 형태로 구현한 것이 스프라이트 애니메이션이다.
셀 애니메이션은 2D 게임 시대의 핵심 기법이었다.
11.1.2 계층적 강체 애니메이션
초기의 3D 캐릭터 애니메이션 기법은 캐릭터를 여러 조각의 강체로 모델링하는 계층적인 강체 애니메이션이었다.
캐릭터 애니메이션에 사용한다면 몸체가 관절에서 갈라지기 때문에 보기에 좋지 않다.
11.1.3 정점 애니메이션과 모프 타겟
정점 애니메이션 기법을 사용하면 애니메이터가 만든 메시의 정점들로부터 게임 엔진에서 실시간으로 움직일 정점 데이터를 추출할 수 있다. 하지만 이 방법을 사용하면 시간에 따라 변화하는 움직임 정보가 메시의 각 정점에 저장되어야 하기 때문에 데이터 양이 매우 커져 실시간 게임에는 잘 사용되지 않는다.
실시간 게임에 사용되는 기법에서는 모프 타겟 애니메이션 기법을 사용한다.
이 방식에서는 애니메이터가 메시의 정점들을 움직여 상대적으로 적은 개수의 고정된 핵심 포즈를 몇 개 만든다.
실행 시에는 이 고정된 핵심 포즈를 두개 혹은 그 이상을 섞어 애니메이션을 생성할 수 있다. 생성되는 애니메이션의 각 정점 위치는 핵심 포즈들의 정점 위치로부터 선형 보간 기법을 사용하여 계산할 수 있다.
모프 타겟 기법은 주로 표정 애니메이션에 사용된다. 사람 얼굴은 50개 이상의 근육으로 이루어진 굉장히 복잡한 조직이기 때문이다.
11.1.4 스킨 애니메이션
정점 애니메이션이나 모프 타겟 애니메이션과 같이 애니메이션되는 메시의 삼각형들을 변형할 수 있다는 장점이 있다.
그러면서도 효율적인 성능과 메모리 사용량을 갖는 계층적 강체 애니메이션의 이점도 함께 갖는다.
뼈대의 관절부분에 스킨이라 불리는 매끄럽고 연속적인 삼각형 메시가 붙는다.
스킨의 정점들은 관절의 움직임을 따라간다.
스킨 메시의 각 정점들은 다수 관절에 가중치를 갖고 결합될 수 있어 관절의 움직임에 따라 자연스럽게 스킨이 늘어날 수 있게 된다.
11.2 뼈대
뼈대는 관절이라고 하는 강체 조각들의 계층으로 이루어져 있다.
11.2.1 뼈대 계층 구조
뼈대의 관절들은 계층 구조나 트리 구조로 형성된다. 하나의 루트 관절이 있고 다른 모든 관절들은 자식이 되거나 자식의 자식이 되는 식으로 연결된다.
보통 각 관절에 0에서 N-1까지 번호를 붙인다. 각 관절마다 부모 관절의 번호를 저장하도록 하면 계층 구조 전체를 저장할 수 있다.
11.2.2 뼈대를 메모리에 저장
일반적인 뼈대의 자료구조
struct Joint
{
Matrix4x3 m_invBindPose; // 관절의 바인드 포즈 역변환
const char* m_name; // 관절의 이름
U8 m_iParent; // 부모 관절의 인덱스 // 8비트로 최대 256개의 관절을 지원 // 루트라면 -1 (0xFF)을 저장
}
struct Skelton
{
U32 m_jointCount; // 관절의 수
Joint* m_aJoint; // 관절의 배열
}
11.3 포즈
11.3.1 바인드 포즈 (= 레퍼런스 포즈, 레스트 포즈, T 포즈)
3D 메시가 뼈대에 연결되기 전의 포즈이다.
뼈대가 연결되지 않은 삼각형만으로 이루어진 원래 상태의 메시 모습이다.
11.3.2 로컬 포즈
관절의 포즈는 대부분의 경우 부모 관절을 기준으로 지정한다.
관절의 포즈는 이동, 회전, 크기 변환의 합성행렬로 나타낼 수 있다.
전체 뼈대의 포즈는 모든 관절의 포즈의 집합이라고 할 수 있다.
11.3.2.1 관절 스케일
포즈나 애니메이션에서 스케일을 생략하거나 제한하면 몇 가지 이득이 있다.
관절 스케일(크기 변환)을 사용하지 않으면 저장 공간을 줄일 수 있다. (크기 행렬 파라미터를 받지 않아도 된다.)
비균일 스케일을 허용하면 관절의 경계 구가 타원체로 변화하지 않는다. 그렇기 때문에 절두체 검사나 충돌 검사를 관절 단위로 하는 엔진에서 필요한 연산을 대폭 줄일 수 있다.
11.3.2.2. 관절 포즈를 메모리에 저장
관절 포즈는 대개 SQT 형태로 저장된다.
struct JointPose
{
Quaternion m_rot; // Q
Vector3 m_trans; // T
F32 m_scale; // S
}
struct SkeletonPose
{
Skeleton* m_pSkeleton; // 뼈대 자료구조, 관절의 개수 정보
JointPose* m_aLocalPose; // 각 관절의 로컬 관절 포즈
}
11.3.2.3 기저 변환으로서의 관절 포즈
관절 j의 좌표 공간을 표현한 점 혹은 벡터에 관절 표준 변환(QTS로 만든 변환)을 적용한 결과는 부모 공간에서의 점이나 벡터가 된다.
11.3.3 글로벌 포즈 (= 모델 공간 포즈)
관절 j의 모델 공간 포즈를 얻으려면 그 관절에서 시작해 뼈대의 계층을 루트까지 거슬러 올라가면서 각 관절의 로컬 포즈를 곱해 나가면 된다.
11.3.3.1 글로벌 포즈 구현
매번 행렬의 곱셈을 계산하기보다는 SkeletonPose 구조체에 글로벌 포즈 변환 행렬을 멤버로 추가하는 방법을 사용한다.
11.4 클립
게임은 캐릭터가 어떻게 움직이고 행동할지 예측할 수 없기 때문에 게임 애니메이션은 길게 이어지는 프레임으로 제작하지 않는다.대신 게임 캐릭터의 움직임을 수 많은 세세한 동작 단위로 끊어서 만든다. 이 개별적인 움직임들을 애니메이션 클립 또는 그냥 애니메이션이라고 부른다.
각 클립에는 캐릭터가 수행하는 잘 정의되는 동작 한 개가 들어 있다.
어떤 클립은 걷거나 뛰기 사이클처럼 루핑되는 것도 있고 한 번씩만 재생되는 것도 있다.
애니메이션을 클립을 사용하지 않고 제작하는 경우는 플레이어와 캐릭터가 상호작용을 하지 않는 부분에 등장하는 IGC, NIS, FMV 장면을 만드는 경우이다.
인 게임 시네마틱(IGC, in-game cinematic), 비상호작용 장면(NIS, non-interactive sequence)는 일반적으로 게임 엔진에서 실시간으로 렌더링 하는 경우를 지칭한다.
풀 모션 비디오(FMV, full-motion video)는 어떤 장면을 미리 MP4나 WMV 또는 다른 형식의 동영상 파일로 만들어 놓은 다음 게임 엔진의 전체 화면 동영상 재생기로 실행 시 재생하는 경우를 말한다.
11.4.1 로컬 타임라인
모든 애니메이션 클립은 로컬 타임라인을 갖고 있다. 보통 독립된 변수 t로 표현하고,
t의 값은 0 (클립의 시작점) ~ T(클립의 전체 재생시간) 범위의 값을 가진다.
11.4.1.1 포즈 보간과 연속 시간
애니메이터가 프레임 단위로 포즈를 잡을 필요는 없다.
애니메이터는 클립 내의 특정 시각에 키 포즈나 키 프레임이라 불리는 주요한 포즈를 만든다.
비어 있는 클립은 컴퓨터를 이용, 선형이나 곡면 보간 등의 방법을 사용하여 채운다.
포즈들 사이를 보간하는 엔진의 기능 덕분에 정수 프레임 뿐만 아니라 실수 프레임에서의 캐릭터 포즈도 추출할 수 있다.
시간 척도 변수를 사용하여 애니메이션을 원래보다 빠르게, 느리게, 거꾸로 재생할 수 있다. 실수 프레임에서의 포즈 추출이 이럴 때 필요하다.
11.4.1.3 프레임과 샘플
프레임이라는 용어는 1/30초 1/60초와 같이 시간 간격을 나타낼 때 쓰고
애니메이션의 특정 시각을 나타낼 때는 샘플이라는 용어를 쓰자.
11.4.1.4 프레임, 샘플, 루핑 클립
루핑 클립에서 첫 번째 포즈와 마지막 포즈는 똑같기 때문에 보통 게임 엔진은 루핑 클립의 마지막 샘플을 생략한다.
루핑 클립이 아닌 경우 N 프레임 애니메이션에는 N + 1 개의 고유한 샘플이 있다.
루핑 클립인 경우 마지막 샘플은 군더더기이므로 N 개의 고유한 샘플이 있다.
11.4.1.5 정규화된 시간(위상)
타임라인 변수의 범위를 0 ~ T에서 0 ~ 1로 정규화 하면 편리한 경우가 있다.
재생시간이 2초(60프레임)인 달리기 애니메이션을 3초(90프레임)인 걷기 애니메이션으로 부드럽게 크로스 페이드 하고 싶 은 경우
11.4.2 글로벌 타임라인
게임의 모든 캐릭터에는 글로벌 타임라인(캐릭터가 처음 게임 월드에 생성된 시점, 특정 레벨이 시작되는 시점 또는 게임이 처음 시작할 때 시작되는 시계)이 있다.
애니메이션을 재생한다라는 것을 클립의 로컬 타임라인을 캐릭터의 글로벌 타임라인에 매핑한다고 생각할 수 있다.
애니메이션 클립을 글로벌 타임라인에 매핑하려면 다음과 같은 클립의 정보를 알고 있어야 한다.
ㄴ 글로벌 시작 시간
ㄴ 재생 비율 R
ㄴ 총 재생 시간 T
ㄴ 반복 재생해야 할 횟수 N
11.4.3 로컬 클록과 글로벌 클록 비교
애니메이션 시스템은 현재 재생중인 모든 애니메이션의 시간 인덱스를 관리하고 있어야 한다.
여기에는 두 가지 방법이 있다.
로컬 클록
ㄴ 각 클립은 고유한 로컬 클록을 갖고 있다. (초 단위, 프레임 단위, 또는 정규화된 시간 단위)
글로벌 클록
ㄴ 캐릭터는 대개 초로 측정되는 글로벌 클록을 갖고, 각 클립은 재생을 시작한 시점의 글로벌 시각을 저장하기만 하면 된다. 클립의 로컬 클록을 이 정보들을 이용하여 계산한다.
글로벌 클록 방식을 사용하면 한 캐릭터를 동기화하는 것뿐만 아니라 여러 캐릭터의 동기도 쉽게 맞출 수 있다.
예를 들어 캐릭터가 주먹으로 때리는 애니메이션과 NPC의 맞는 애니메이션을 동기화 하는 경우 로컬 클록의 방식에서는 두 애니메이션 사이에 프레임 지연이 발생할 가능성이 있다.
11.4.4 간단한 애니메이션 데이터 형식
struct AnimationSample
{
JointPose* m_aJointPose; // 애니메이션의 샘플 한 개는 뼈대 내 각 관절들의 모든 포즈로 이루어진다.
};
struct AnimationClip
{
Skeleton* m_pSkeleton; // 뼈대 자료구조
F32 m_framesPerSecond; // 초당 프레임 수
U32 m_frameCount; // T(총 재생 시간, 전체 샘플 수)
AnimationSample* m_aSamples; // 샘플의 배열
bool m_isLooping; // 루프 클립인지 아닌지
}
실제 게임 엔진에서는 애니메이션을 이러한 형식 그대로 저장하지는 않는다. 보통은 저장 공간을 줄이려고 여러 방식으로 애니메이션 데이터를 압축한다.
11.4.4.1 애니메이션 리타겟팅
애니메이션은 한 가지 뼈대에만 적합하지만, 비슷한 구조로 이루어진 뼈대들의 경우에는 예외다. 예를 들어 기본 계층구조에 영향을 미치지 않는 부가적인 말단 관절만 다르고 전체 뼈대의 구조는 동일한 경우에는 애니메이션을 공유할 수 있다.
이렇게 하려면 현재 애니메이션 되는 뼈대에는 없는 관절인데 이것의 채널 데이터가 있다면 그냥 무시하도록 엔진을 만들어야 한다.
11.4.5 연속 채널 함수
애니메이션 클립의 샘플은 시간에 대한 연속함수로 정의할 수 있다.
대다수 게임 엔진은 샘플을 선형적으로 보간하는데, 이 경우 사용되는 함수는 원래의 연속함수를 구분적 선형근사(piece-wise linear approximation)한 것이다.
11.4.6 메타 채널
많은 게임에서는 애니메이션에 필요한 부가적인 데이터를 사용할 수 있도록 메타 채널을 허용한다.
메타 채널에는 뼈대의 포즈를 잡는 일에는 직접 영향을 미치지는 않지만 애니메이션과 동기화해야 하는 게임에 특화된 정보를 담을 수 있다.
흔히 다양한 시간 인덱스(샘플)에서의 이벤트 트리거를 저장하는 특별한 채널을 정의한다.
애니메이션의 로컬 시간 값이 트리거를 지날 때 마다 이벤트를 게임 엔진으로 보내 필요한 처리를 할 수 있도록 한다.
예를 들어 애니메이션 특정 시점에서의 소리나 파티클 효과를 재생하는데 사용한다.
11.5 스키닝과 행렬 팔레트 생성
스키닝 : 3D 메시의 정점들을 포즈를 잡은 뼈대에 연결 하는 과정
11.5.1 정점별 스키닝 정보
skinned mesh는 정점에 의하여 뼈대에 연결된다. 각 정점은 관절 한 개이상의 관절에 묶일 수 있다.
관절 한 개에만 묶인 정점은 관절의 움직임과 똑같이 움직인다. (붙어있다.)
두 개 이상의 관절에 묶인 정점은 각 관절을 따라 따로 움직인 경우의 위치를 구한 후, 각 위치를 가중치에 따라 가중 평균하여 최종 위치를 결정한다.
3D 아티스트가 메시의 각 정점에 다음과 같은 정보를 지정해야 한다.
ㄴ 정점이 묶일 관절의 인덱스
ㄴ 정점과 묶인 관절들의 가중치
보통 하나의 정점에 묶일 수 있는 관절의 수는 4개를 최대로 잡는다. 관절 인덱스 표현을 8비트씩 4개로 32비트 워드 하나에 들어가도록 하면 좋기 때문에
가중치 4개의 합은 1이기 때문에 정점에는 3개의 가중치만 저장한다.
11.5.2 스키닝 수학
메시의 정점을 바인드 포즈에 맞는 위치에서 현재 뼈대의 포즈에 맞는 위치로 변환해 줄 행렬이 필요한데 이를 스키닝 행렬이라고 부른다.
스킨드 메시의 정점도 모델 공간에서 정의된다.
모든 관절마다 스키닝 행렬을 계산해야 한다. (= 스키닝 행렬의 배열 = 행렬 팔레트)
애니메이션 엔진이 각 관절의 스키닝 행렬을 계산하는 순서
1. 각 관절의 로컬 포즈를 계산
2. 로컬 포즈를 글로벌 포즈로 바꾸기
3. 캐시된 관절의 바인드 포즈의 역행렬과 곱하기
11.5.2.3 모델-월드 변환 통합
모든 정점은 언젠가는 모델 공간에서 월드 공간으로 변환되어야 한다. 따라서 어떤 게임 엔진은 스키닝 행렬 팔레트에 물체의 모델 월드 변환을 먼저 곱해 놓는다.
한 애니메이션을 동시에 여러 캐릭터에 적용해야 한다면 모델-월드 변환을 통합하면 안된다.
11.5.2.4 다관절에 묶인 정점의 스키닝
정점에 스키닝 행렬을 곱하고 가중치를 곱한 값들의 합을 새로운 정점으로 사용한다.
11.6 애니메이션 블렌딩
특정한 시각에서 두 개 이상의 포즈를 합쳐 같은 시각에서의 출력(하나의 포즈)을 만든다.
짧은 시간에 걸쳐 소스 애니메이션에서 목적 애니메이션으로 부드럽게 전환하는데 일시적으로 블렌딩을 사용할 수 있다.
11.6.1 LERP 블렌딩
LERP 연산을 SQT의 각 성분에 개별적으로 적용한다.
T는 벡터 LERP
Q는 사원수 LERP나 SLERP
S는 벡터 LERP
두 뼈대 포즈를 선형 보간할 때 가장 자연스로운 중간 포즈는 관절의 부모 공간(로컬 공간)에서 각 관절 포즈를 독립적으로 보간할 때 나온다. 포즈 블렌딩은 로컬 공간에서 하기 때문에 각 관절 포즈의 선형 보간은 뼈대의 다른 관절들에 완전히 독립적이다. 즉 포즈를 선형 보간하는 일은 멀티프로세서 구조에서 완전히 병렬로 수행할 수 있다는 뜻이다.
11.6.2 LERP 블렌딩 적용
11.6.2.1 시간 블렌딩
샘플된 포즈들 사이의 중간 포즈를 구할 때 사용한다.
t = 2, t = 3에서의 샘플된 포즈만 알고 있을 때, 만약 t= 2.18에서의 샘플을 얻고 싶다면 t = 2와 t = 3 에서의 포즈를 가중치 비율 0.18로 보간하면 된다.
11.6.2.2 움직임 연속성, 크로스 페이딩
클립 전환 중에도 캐릭터 신체 각 부분이 완벽하게 부드럽게 움직이도록 하는 것이 목표이다. (정점들의 위치가 연속적으로 바뀌어야 한다. = C0 연속성)
1차 도함수 또한 연속적이어야 한다.(C1 연속성)
LERP 기반 애니메이션 블렌딩을 사용할 경우 보기 좋은 C0 연속성을 구현할 수 있고 C1 연속성에 근접한 효과를 낼 수도 있다.
클립 사이의 전환에 LERP 블렌딩을 활용하는 것을 크로스 페이딩이라고 부르기도 한다.
크로스 페이딩의 종류
부드러운 전환
ㄴ 동기화가 잘 되어 있어야 한다.
ㄴ 블렌딩 계수를 0부터 1까지 증가시키며 두 클립을 동시에 재생한다.
동결 전환
ㄴ 이전 클립의 로컬 클록은 이후 클립이 재생을 시작하는 순간 멈춘다.
ㄴ 두 클립이 별로 연관성이 없고 시간을 동기화할 수 없는 경우에도 잘 동작한다.
블렌딩 계수를 일차원 베지어와 같은 시간의 3차 함수로 만들면 더 부드럽게 전환하게 만들 수도 있다.
11.6.2.3 방향 이동
타겟 이동 (targeted movement)
ㄴ 애니메이터가 전진 이동, 왼쪽 스트레이핑, 오른쪽 스트레이핑 세 종류의 루핑 애니메이션을 만들어야 한다.
ㄴ 이동 방향에 따라 인접한 클립을 섞는다. (전진, 왼쪽)을 섞거나 (전진, 오른쪽)을 섞는다. 이동 방향의 각도가 두 인접한 클립에 얼마나 가까운지에 따라 블랜드 비율을 정한다.
선회축 이동 (pivotal movement)
ㄴ 캐릭터의 수직축을 기준으로 캐릭터를 회전하면서 그냥 앞으로 이동하는 루프 애니메이션을 재생
11.6.3 복합 LERP 블렌딩
11.6.3.1 일반적인 1차원 LERP 블렌딩
1차원 범위 안에 다수의 애니메이션 클립과 블렌딩 계수를 새로 정의하여 블렌딩 계수와 인접한 두 클립을 블렌딩한다.
11.6.3.2 단순한 2차원 LERP 블렌딩
2차원 범위 에서 인접한 4개의 클립을 블렌딩한다.
11.6.3.3 삼각형 2차원 LERP 블렌딩
2차원 블렌딩 벡터 b가 주어지면 2차원 블렌딩 공간에서 세 클립으로 형성되는 삼각형의 무게중심 좌표를 계산함으로써
3개의 블렌딩 가중치 값을 구할 수 있다. 3개의 클립을 블렌딩한다.
11.6.3.4 일반적인 2차원 LERP 블렌딩
2차원 블렌딩 공간에 임의로 배치도니 임의 갯수 애니메이션 클립에도 무게 중심을 구하는 방법을 확장할 수 있다.
들로네 삼각분할 기법을 이용해 임의의 점들로 이루어진 삼각형들을 구하는 방법이다.
11.6.4 부분-뼈대 블렌딩
사람은 신체 각 부분을 따로 움직일 수 있다. 예를 들면 걸으면서 오른손을 흔들고 왼손으로는 뭔가 가리킬 수 있다.
부분 뼈대 블렌딩 이라고 부르는 기법을 이용하면 게임에서 이와 같은 움직임을 구현할 수 있다.
각 관절마다 서로 다른 블렌드 비율을 쓸 수 있도록 하면 된다.
블렌드 마스크를 사용하여 특정한 관절의 블렌드 비율을 0으로 설정하여 특정 관절에 애니메이션이 적용되지 않게 할 수 있다.
부분-뼈대 블렌딩은 캐릭터 움직임이 부자연스러운 문제가 있다.
ㄴ 관절별 블렌드 인자가 급격하게 변하면 신체의 일부분이 나머지 부분과 따로 움직이는 것처럼 보인다.
ㄴ 사람의 신체는 절대 독립적으로 움직이지 않기 때문에 부자연스럽다.
11.6.5 가산 블렌딩
가산 블렌딩은 두 애니메이션 클립의 차이를 나타내는 차이 클립을 만든다.
한 번 차이 클립을 만들면 원래의 참조 클립 뿐만 아니라 전혀 상관없는 다른 클립들에도 더할 수 있다.
예를 들어 (지친 상태로 뛰는 애니메이션 - 평범하게 달리는 애니메이션)을 걷기 애니메이션에 더하면
지친상태로 걷는 애니메이션이 된다.
11.6.5.1 수학 공식
D = S(소스 클립) - R(참조 클립) 인데 관절 포즈는 자기 관절의 공간을 부모 관절의 공간으로 변환 하는 행렬이다.
행렬 연산에서 - 는 역행렬의 곱셈이므로 관절의 차이 포즈는
소스 포즈 행렬에 참조 포즈의 역행렬을 곱한 것이다.
입력 클립 S와 R의 재생시간이 같을 때만 차이 클립을 구할 수 있다.
11.6.5.3 가산 블렌딩의 한계
기존 애니메이션에 움직임을 더하는 방식을 쓰기 떄문에 뼈대의 관절을 과도하게 회전시키는 경향이 있다.
문제를 피하기 위한 팁
ㄴ 참조 클립의 엉덩이 관절 회전을 최소화한다.
ㄴ 참조 클립의 어깨와 팔꿈치 관절을 중립 위치에 두어 팔이 과도하게 돌아가는 것을 방지한다.
ㄴ 애니메이터는 각 코어 포즈마다 새로운 차이 클립을 만들어야 한다.
11.6.6 가산 블렌딩 활용
ㄴ 발 자세 변이
ㄴ 움직임 소음
ㄴ조준과 바라보기
ㄴ시간 축의 재해석
11.7 후처리
마야와 같은 애니메이션 도구를 이용해 export한 데이터를 이용한 것이 아니라 실행 시간에 생성하는 애니메이션을 절차적 애니메이션이라고 한다.
수작업으로 만든 애니메이션 클립에서 뼈대의 포즈를 먼저 잡고, 절차적 애니메이션으로 포즈를 수정하는 후처리 작업을 한다.
11.7.1 절차적 애니메이션
원래의 애니메이션의 포즈를 후처리하기 위해서 변화가 필요한 관절의 Q, S, T 채널을 변경시켜 최종 포즈를 결정하게 한다.
11.7.2 역운동학 (IK, inverse kinematics)
캐릭터가 물체를 집어 드는 애니메이션 클립에서 캐릭터가 물체를 잡도록 뼈대의 최종 포즈를 조정해 목표하는 물체와 일치시키고자 할 때 사용하는 기법
역운동학에서는 한 관절의 글로벌 포즈(end effector)가 원하는 위치로 움직일 수 있도록 다른 관절들의 로컬 포즈를 변경하는 것이 목표이다.
11.7.3 래그 돌
캐릭터가 죽었을 때 몸이 축 늘어진다. 이러한 상황에서는 몸이 주변 환경에 따라 물리적으로 현실감 있게 반응하여야 한다. 이런 목적으로 래그 돌을 사용한다.
11.8 압축 기법
한 개의 관절 포즈에 부동소수점 수 10개가 필요할 수 있다(평행이동 3개, 회전변환 4개, 스케일변환 3개)
뼈대 관절이 100개이고, 30프레임으로 샘플된 1초 길이의 클립이라면
10 * 4바이트 * 100 * 30 = 12000 바이트
1초당 117KB의 메모리를 사용한다.
11.8.1 채널 생략
애니메이션 클립의 크기를 줄이는 가장 단순한 방법은 관계없는 채널을 생략하는 것이다.
대부분의 캐릭터는 불균등 스케일을 사용할 필요가 없기 때문에 스케일 채널을 3개에서 1개로 줄일 수 있다.
캐릭터의 뼈들은 일반적으로 늘어나지 않기 때문에 루트 관절이나 얼굴 관절, 떄로는 쇄골뼈의 관절을 제외하고는 평행이동도 생략할 수 있다. (3개 -> 0개)
사원수는 언제나 정규화되기 때문에 4개의 성분중 3개만 저장하고 마지막 성분은 실행 시에 계산할 수 있다. (4개 -> 3개)
전체 재생시간 동안 포즈가 변하지 않는 관절의 채널은 t=0에서만 기록하고 한 비트 값으로 남은 시간동안 변하지 않는다는 것을 표시하는 방법으로 채널을 생략할 수 있다.
11.8.2 양자화
각 채널의 크기를 32비트 부동소수가 아닌 16비트 정수로 바꿔 표현하는 방법
부동소수점 수 값을 정수로 바꾸는 과정을 인코딩
정수를 부동소수점 수 값으로 바꾸는 과정을 디코딩
11.8.3 샘플링 주기와 키 생략
애니메이션 데이터가 큰 이유
1. 각 관절에 10개의 부동소수점 수를 갖는 채널이 있어서
2. 뼈대에 관절이 많아서
3. 애니메이션 샘플링 빈도가 높아서 (30프레임)
1번 이유는 채널 생략으로 극복할 수 있다.
2번 이유는 고품질 캐릭터의 관절 갯수를 줄이기는 힘들기 때문에 해결하기 힘들고
3번 이유는
ㄴ 샘플링 빈도 낮추기 : 30프레임에서 15프레임으로 낮추면 애니메이션 데이터를 절반으로 줄일 수 있다.
ㄴ 샘플 생략하기 : 클립에서 일정 시간 동안 채널 데이터가 선형에 가까운 변화를 보인다면 이 기간 동안의 샘플은 양 끝점들을 제외하고 모두 생략할 수 있다. 샐행 시에 선형 보간을 하여 생략된 샘플들을 복원한다.
11.8.4 커브 기반 압축
애니메이션을 일정 시간 간격의 포즈 샘플이 아니라 각 관절의 S, Q, T 채널들의 시간에 따른 궤적을 n차원, 불균등, 비유리수 B-스플라인의 집합으로 기록하는 방법이다.
B-스플라인을 사용하면 많은 굴곡을 갖는 채널들을 몇 개의 데이터로 인코딩할 수 있다.
11.8.5 선택적 로딩과 스트리밍
애니메이션이 특정 레벨에서만 쓰인다면 그 레벨이 로딩될 때 같이 로드하고 레벨이 끝날 때 해제하도록 하면 메모리를 잘 관리할 수 있다.
11.9 애니메이션 시스템 아키텍처
대부분의 애니메이션 시스템들은 최대 세 개의 개별 레이어들로 구성된다.
애니메이션 파이프라인
ㄴ 게임에서 애니메이팅되는 캐릭터와 객체에 대하여 하나 이상의 애니메이션 클립들과 블렌드 적용 값을 입력으로 블렌딩하여 하나의 로컬 뼈대 포즈를 출력한다.
ㄴ 각 관절의 스키닝 행렬, 글로벌 뼈대 포즈를 계산하여 출력한다.
액션 상태 머신
ㄴ 게임 캐릭터의 동작들은 유한 상태 머신으로 모델링하는 것이 제일 좋은 방법이다.
ㄴ 동시에 여러 동작을 할 때, 캐릭터 몸체의 각 부위들이 독립적으로 동시에 움직일 수 있도록 해준다.
애니메이션 컨트롤러들
ㄴ 특정 상황에 처한 캐릭터의 행동을 관리하는 시스템
11.10 애니메이션 파이프라인
1. 클립 압축 해제와 포즈 추출
ㄴ 각 입력 클립에 대한 로컬 뼈대 포즈를 출력한다.
ㄴ 이 포즈가 가지는 정보는 뼈대에 있는 모든 관절에 대한 정보이거나, 관절의 일부분에 대한 정보이거나, 첨가 블렌딩에서 사용되는 차이 포즈가 될 수 있다.
2. 포즈 블렌딩
ㄴ 하나 이상의 애니메이션 클립을 블렌딩할 때만 실행되는 단계, 뼈대에 있는 모든 관절들에 대한 하나의 로컬 포즈를 출력한다.
3. 글로벌 포즈 생성
ㄴ 뼈대에 대한 계층구조를 따라서 지역 관절들을 조합하여 뼈대에 대한 글로벌 포즈를 생성한다.
4. 후처리
ㄴ 포즈를 마무리 하기 전에 뼈대의 로컬/글로벌 포즈를 수정한다.
ㄴ 후처리 단계에는 IK, ragdoll physics 기법을 적용할 수 있다.
5. 글로벌 포즈가 최종적으로 만들어지고 난 후 각 관절들의 글로벌 포즈 행렬을 그것과 일치하는 바인드 포즈 역행렬과 곱하여 렌더링 엔진 입력에 알맞는 스키닝 행렬 팔레트(배열)을 출력으로 생성한다.
11.10.1 자료 구조
11.10.1.1 공유 자원 데이터
모든 게임 엔진 시스템들은 공유 자원 데이터와 인스턴스별 상태 정보를 명확하게 구별해야 한다.
일반적으로 같은 유형의 캐릭터나 객체들은 하나의 자원 데이터를 공유하여 사용한다.
ㄴ 뼈대 - 뼈대는 관절과 관절 계층 구조를 묶어서 만든 포즈를 나타낸다.
ㄴ 스킨 메시 - 하나의 뼈대에는 여러 개의 스킨 메시들이 붙을 수 있다.
ㄴ 애니메이션 클립 - 한 캐릭터의 뼈대에 많은 애니메이션 클립이 적용될 수 있다.
게임에서 사용하는 고유한 뼈대들의 수를 최소한으로 줄이는 것이 도움이 된다.
11.10.1.2 인스턴스별 데이터
특정 캐릭터 형태에 대한 인스턴스 정보
1. 클립 상태
ㄴ 로컬 클록, 재생 비율
2. 블렌드 명세
ㄴ 블렌드 노드 트리 방식 : 블렌드 트리를 공유 자원으로 처리한다.
ㄴ 가중 평균 방식 : 블렌드 가중치를 인스턴스 별 상태 정보의 일부분으로 저장한다.
3. 부분 뼈대 관절 가중치
ㄴ 부분 뼈대를 블렌딩하게 되면 각 관절이 마지막 포즈에 미치는 영향 정도를 관절 가중치로 지정한다.
4. 로컬 포즈, 글로벌 포즈, 매트릭스 팔레트
11.10.2 균일 가중 평균 블렌딩 표현
모든 활성 애니메이션 클립(가중치가 0이 아닌 클립)들은 리스트로 관리된다.
N개의 활성 애니메이션들에서 평행 이동벡터들, 회전 사원수, 스케일 값들을 추출하여 가중평균을 계산한다. 이렇게 해서 뼈대의 마지막 포즈를 만든다.
11.10.3 블렌드 트리 (일반적으로 많이 씀)
애니메이션 블렌드 트리의 내부 노드들은 연산자이고 말단 노드들은 연산자에 제공되는 입력을 나타낸다.
ㄴ 11.6.3 복합 Lerp 블렌딩에서 다룬 내용들이다.
11.10.4 크로스-페이딩 아키텍처
크로스 페이드는 에니메이션 엔진이 균일 가중 평균을 사용하는지 또는 수식 트리 구조(블렌드 트리)를 사용하는지에 따라 두 가지로 구현될 수 있다.
11.10.4.1 균일 가중 평균 방식을 이용한 크로스-페이드
클립들의 가중치들을 조정하여 크로스-페이드를 구현한다.
복합 블렌드에서 다른 복합 블렌드로 전환하는 것을 크로스 페이딩으로 처리하는 것이 약간 까다롭다.
ㄴ 클립 그룹의 가중치의 합이 항상 1이도록 유지해야한다. 복합 블렌드를 전환하는 경우 가중치의 값이 0과 1 사이일 때 각 그룹의 가중치들을 더해도 1이 되지 않을 수 있기 때문에 그룹이 어떻게 묶여 있는지를 저장하는 메타 데이터가 더 필요하게 된다.
11.10.4.2 수식 트리를 이용한 크로스-페이드
복합 블렌드에서 다른 블렌드로 전환하든지 클립에서 클립으로 전환하든지 동일한 방식을 사용한다.(직관적이다.)
11.10.5 애니메이션 파이프라인 최적화
하드웨어에 따라 고유한 최적화 문제가 있다.
캐시 미스와 로드-적중-저장 연산을 피해야 한다.
부동소수점 연산을 피해야한다.
11.11 액션 상태 머신
액션 상태 머신은 애니메이션 파이프라인 위에 위치하며, 게임에서 캐릭터의 액션들을 상태 기반으로 직접 조정할 수 있도록 해준다.
11.11.1 애니메이션 상태
액션 상태 머신에서 각 상태는 동시에 처리되는 애니메이션 클립들에 대한 임의의 복합 블렌드에 해당된다.
특정 애니메이션 상태에 해당하는 블렌드 트리는 단순하거나 복잡할 수 있다 예를 들어 대기 상태는 몸 전체 애니메이션 하나에 적용될 수 있고, 달리기 상태는 왼쪽 스트레이핑, 전방달리기, 오른쪽 스트레이핑을 각각 -90도 0도 90도로 섞는 반원형 블렌드에 해당된다.
11.11.2 상태 전환
11.11.2.1 상태 전환의 종류
소스 상태의 마지막 포즈와 목적 상태의 첫 번째 포즈가 정확하게 일치하다면, 한 상태에서 다른 상태로 바로 전환시킬 수 있다. 그렇지 않으면 크로스 - 페이드 방식으로 전환해야 한다.
크로스-페이드 방식이 부자연스러운 경우 한 상태에서 다른 상태로 변할 때만 사용할 전환 상태라는 특수한 상태를 상태 머신에 구현한다.
11.11.2.2 상태 전환 파라미터들
2가지 상태들 사이의 특정 전환을 표현할 때 조정할 수 있는 파라미터
ㄴ 소스와 목적지 상태들
ㄴ 전환 타입 (바로 전환인지, 크로스-페이드 인지, 전환 상태를 통해 실행 하는지)
ㄴ 기간 (크로스 페이드가 얼마 동안 일어나야 하는지)
ㄴ ease-in / ease-out 곡선 타입
11.11.2.3 상태 전환 행렬
n개의 상태를 가지는 상태 머신에서 가질 수 있는 전환의 개수는 최대 n의 제곱개 이다.
2차원 정방행렬을 사용하여 세로 축에 있는 상태에서 가로 축에 있는 다른 상태로 전환 가능한 모든 경우를 나타낼 수 있다.
11.11.3 상태 레이어들
상태 머신은 한 순간에 하나의 상태만 가질 수 있기 때문에 몸에서
상태 계층을 사용하여 문제를 해결 할 수 있다.
n개의 각 계층에서 블렌드 트리를 계산하고 n개의 골격 포즈를 생성하고 블렌딩함으로써 뼈대의 최종 포즈를 계산한다.
11.11.4 컨트롤 파라미터들
블렌드 가중치, 재생 비율, 컨트롤 파라타미터를 조정하는 것은 쉬운 일이 아니다.
블렌드 가중치는 블렌딩에서 서로 다른 효과를 낸다. 어떤 가중치는 움직임 방향을 조정할 수 있고, 어떤 가중치는 캐릭터의 움직임 속도를 조정할 수도 있다. 따라서 상위 코드에서 블랜드 가중치들에 접근하는 방법이 있어야 한다.
노드 검색
ㄴ 상위 레벨의 코드에서 트리에 있는 블렌드 노드를 찾는 방법을 제공한다.
이름을 가진 변수
ㄴ 각 컨트롤 파라미터에 이름을 부여하고, 코드는 컨트롤 파라미터를 이름으로 찾아서 원하는 값으로 조정할 수 있다.
컨트롤 구조체
ㄴ 부동 소수점이나 구조체의 배열같은 간단한 자료 구조로 전체 캐릭터에 대한 컨트롤 파라미터들을 모두 저장하여 관리하고 블렌드 트리의 노드는 배열의 인덱스를 통해 찾는다.
'읽은 책 > 게임 엔진 아키텍처' 카테고리의 다른 글
14. 게임플레이 시스템의 소개 (0) | 2022.08.17 |
---|---|
12. 충돌과 강체 역학 (0) | 2022.08.17 |
10. 렌더링 엔진 (0) | 2022.08.12 |
9. 디버깅과 개발 도구 (0) | 2022.08.10 |
8. 휴먼 인터페이스 장치 (HID) (0) | 2022.08.09 |