사용자:Senouis/Xash3D 분석

리버티게임, 모두가 만들어가는 자유로운 게임

Xash3D는 미국의 밸브 코퍼레이션의 첫 게임 엔진인 하프라이프 엔진(흔히 GoldSource 엔진으로 알려졌다)과 호환되면서 Open-source, cross-platform을 지향하는 게임 엔진이다. 하프라이프는 게임 소스 코드 공개를 통한 2차 창작 MOD를 장려하였기 때문에 Xash3D 엔진으로 돌아가는 게임이 많다.

현재는 FWGS(Flying With Gauss의 약어라지만 뭔가 Freeware GoldSource의 약어 같기도 하다)이라는 이름의 동인 개발 서클에 의해 관리되고 있다(러시아 쪽 다큐먼트가 거의 공식 영어 도큐먼트 수준으로 자세하고 러시아 쪽에서 인기 있는 모드의 지원이 많아 러시아인들이 주축이 된 팀으로 보인다)

Xash3D 엔진의 최신 소스 코드는 여기에서 확인할 수 있다.

한편 Emscripten 트랜스컴파일러로 javascript 코드로 C/C++ 코드 컴파일을 한 Github 리포지토리가 있다. 링크

Xash3D 개요[편집 | 원본 편집]

  • 개발 언어: C/C++
  • 지원 플랫폼: 윈도우즈 / 맥 / 리눅스 계통 / iOS / 안드로이드 / Nintendo 3DS

과거에는 Emscripten을 통한 웹 버전으로 컴파일이 되었으나 CMake 대신 Waf로 빌드 스크립트 구성 프로그램을 바꾸는 바람에 지원이 중단되었다. 따라서 Xash3D의 현 버전은 웹에 적용하려면 지나치게 많은 비용이 든다. 그러나 2000년대 게임들을 돌리는 것을 주 목적으로 하기에 언리얼 엔진 / 유니티 엔진에 비해 코드가 간결하다는 점에서 레거시 버전이라도 리버티게임에 가젯 형태로 자바스크립트로 트랜스컴파일된 물건을 포팅하는 것이 의미가 있다.

Xash3D 전체 구조[편집 | 원본 편집]

Xash3D는 크게 filesystem IO를 담당하는 라이브러리, 메인 메뉴 UI 관련 코드가 있는 라이브러리, 핵심 엔진 기능을 담은 라이브러리, 그리고 그래픽스 라이브러리가 있다. 이 중 레거시가 된 웹 버전인 상단의 HTML5 버전 레포지토리는 다음과 같이 javascript 파일이 생성되었다.

  • 파일시스템 IO: browserfs.min.js(browserfs 라이브러리의 최소 기능 컴파일 버전)이 대행하여 애셋 압축 파일을 읽는다.
  • 메인 메뉴 UI: menu.js
  • 게임 엔진 본체: xash.js
  • 클라이언트/서버 측에서만 돌아가는 게임 코드: client.js, server.js

그 외에는 게임별 애셋 로드 코드 등을 개발자가 직접 추가할 수 있다.

Xash3D의 구동 과정[편집 | 원본 편집]

사실 모든 게임 엔진은 다음 구조로 동작한다.

  • 게임 실행 파일이 xash 라이브러리 파일(어떻게 컴파일했는지에 따라 xash.js, xash.dll, ...) 가져온 다음, 클라이언트 측 코드와 서버 측 코드를 가져와 진입점으로 들어간다.
    • 특정 플랫폼에서만 작동하는 네이티브 코드의 경우 Client 측에서 Initialize, VidInit를 호출하고 Server 측에서는 GameDLLInit를 호출한다. 웹 포트도 그러한 구조일 것으로 추측할 수 있다.
  • 엔진은 시작할 때 기본 게임 폴더(별도로 컴파일 옵션을 주지 않았다면 valve 폴더) gfx.wad를 읽어 로딩 안내 등의 정보를 이미지로 표시한다. 따라서 리버티게임에서는 반드시 gfx.wad를 이름만 같은 다른 파일로 바꿔서 MOD를 배포해야 한다.
  • 게임 로직은 서버 측 코드에 다 적혀 있고, 클라이언트 측은 주로 입력 처리, 네트워크 데이터를 메세지로 받아 처리한 다음 HUD 표시 및 뷰포트와 카메라, 렌더링 및 그에 필요한 간단한 수학 행렬 연산 정도만 한다.
  • 서버 측에서는 주로 게임 모드, 네트워크로 전송할 데이터 값, 개발 콘솔 창에서 쓸 수 있는 변수, 사용자로부터 날아온 각종 이벤트(입력, 무기 사용 등)에 대한 콜백, 그리고 각 엔티티별 코드로 구성되어 있다.
    • 게임 모드의 경우 CGameRules 클래스에서 파생하고, 네트워크 데이터 전송 값은 REG_USER_MSG 매크로, 콘솔 변수는 GET_CVAR_***나 CVAR_REGISTER 매크로를 사용해 크기와 종류를 결정하고, 이벤트는 EV_HookEvent에 이벤트 콜백 함수를 등록하며, 엔티티별 코드는 CBaseEntity 클래스에서 파생한다.

그래서 사실 CBaseEntity와 CGameRules 클래스 및 그 특성을 상속한 클래스들 정도만 컴파일해도 이론상으로는 서버 측 코드가 작동한다. 다만 그러면 할 수 있는 것이 없어 보통 하프라이프 코드를 가져와 고치는 식으로 게임 제작이 이루어진다.

다음은 애셋에 관한 설명이다.

  • 애셋의 경우 배포할 때 하프라이프의 애셋을 전부 그대로 재배포했다간 (하프라이프를 정당한 가격 지불 없이 플레이할 수 있어) 밸브 코퍼레이션이 DMCA Takedown 등의 제재를 먹이므로, 반드시 Half-life SDK에 기반 데이터가 있는 애셋만 남기고 나머지는 제거한 뒤에 배포해야 한다. 이에 따라 쓸 수 있는 애셋은 다음과 같다.
    • 텍스처 파일: decals.wad, halflife.wad, liquids.wad, spraypaint.wad, xeno,wad - 사실상 전부 다 쓸 수 있다(엔진 측에서 사용하는 gfx.wad만 교체할 것).
    • 스프라이트 파일(이펙트 등을 표현): Half-life SDK에 소스 데이터가 있는 explode02.spr (Sprite Tools 하위 폴더의 bmp 폴더에 기본 제공) - 나머지는 로드하지 않도록 소스 코드를 수정해야 한다.
    • 3D 모델링: 다음에 나열할 모델링을 제외하면 사용할 수 없고, 따라서 나머지 관련 코드를 쳐내야 한다. Half-life SDK에서 studiomdl.exe 실행 파일을 사용하여 qc 파일과 smd 파일을 합치면 MDL 파일을 얻을 수 있다. smd 파일의 수정은 Blender Source Tools을 사용해 블렌더(Blender) 프로그램으로 모델링 Import를 하면 가능하며, qc 파일은 메모장으로도 편집이 가능하다.
      • 플레이어 모델링: Half-life SDK의 Player Models 하위 폴더에 소스 데이터가 있는 파일들(Blender Source Tools을 사용해 블렌더로 모델링 Export) - 고든 프리맨의 외형 및 애니메이션은 player 폴더에, 데스매치에서 활용하도록 배포된 NPC(과학자, 군인, 경비원, 그리고 G맨과 로봇 등)은 DMatch 폴더의 것을 사용한다.
      • 프롭(물건) 모델링: Half-life SDK의 Props Models 하위 폴더에 있는 MDL 파일들(별도의 컴파일 필요 없이 바로 Models 폴더에 놓을 수 있음)
      • 무기 모델링: Half-life SDK의 Weapon Models 하위 폴더에 소스 데이터가 있는 파일들(역시 Blender Source Tools 사용 필요), 고든 프리맨이 쓰던 무기들과 탄환 모델링 데이터가 있다.
      • 몬스터 모델링: Half-life SDK의 Weapon Models 하위 폴더에 소스 데이터가 있는 파일들(Blender Source Tools 사용) - 하프라이프의 일부 몬스터와 데스매치 클래식 MOD의 모델링들이 있다.
    • 맵 파일: c1a0.rmf, c1a0d.rmf를 컴파일한 c1a0.bsp 및 c1a0d.bsp - 기본 해머 에디터보다 J.A.C.K을 사용하여 편집하는 것을 권장한다.
    • 사운드 파일: 없음 - 모든 wav 파일을 가져오고 재생하는 코드들을 소스 코드에서 제거해야 한다
  • 하프라이프의 dll 파일 및 애셋이 들어가는 valve 폴더로 Xash3D에서 돌리려면 다음 폴더 구조를 반드시 요구한다.
    • cl_dlls, dlls 폴더: 게임 코드 폴더. 각각 클라이언트 DLL 파일과 서버 DLL 파일이 들어간다. 리버티게임 Xash3D에서는 사용하지 않는다.
    • events: 무기의 동작 등에 대한 이벤트 제어 스크립트(sc 파일)이 들어간다. 재배포에 큰 문제는 없어 보이나, 밸브에서 만든 것이므로 따로 작성하는 것이 안전하다.
    • gfx: 엔진에서 사용하는 파일들을 저장한다. 리버티게임의 Xash3D MOD를 위해 따로 이미지 파일을 몇 개 만들 필요가 있다.
      • gfx/env: 스카이박스(Skybox)의 텍스처로, 리버티게임에서 이것을 안 쓸 필요가 있다. 그냥 검은색으로 칠해진 더미 이미지 하나만 두자.
      • gfx/shell: kb_act.lst와 kb_def.lst가 있는데 전자는 입력 명령어에 설명을 매칭하며, 후자는 키보드 키에 입력 명령어를 매칭하는 데이터 테이블 파일들이다. 마찬가지로 따로 제공해야 한다.
      • gfx/vgui: HUD나 메뉴 화면에서 사용하는 이미지 파일. 필요한 것만 대체하고 나머지는 지운다.
    • maps: 맵 파일 폴더
    • models: 모든 모델링 파일을 때려박는 곳.
    • resource: 딴 건 몰라도, valve_english.txt 파일은 반드시 작성하자. 안 그러면 설정 메뉴가 번역이 안 된다.
    • sound : 사운드 파일 폴더. 배경음악과 효과음들(무기 발사음은 물론, 걸어다니는 소리나 NPC 대사)이 있다.
    • sprites : 게임이 사용하는 스프라이트 이미지 파일.
    • 그 외에 게임이 사용하는 기본 파일인 gameinfo.txt에서는 게임의 진입점 관련 정보(게임 코드 DLL 파일 이름, 게임 시작점이 되는 맵 같은 거)를 기록해야 하는데 개발할 때 xash3d.exe를 실행하면 알아서 생기며 적절히 수정하고 배포하면 된다.

반드시 구현해야 하는 함수[편집 | 원본 편집]

분석이 불완전할 수 있어 계속 업데이트되는 부분이다.

클라이언트(client.js)[편집 | 원본 편집]

다음 함수들은 공통적으로 DLLEXPORT 접두사가 붙은 C언어 함수로, Xash3D가 클라이언트 렌더링에 필요로 하는 게임 코드의 함수들이다. 하프라이프 1의 원본 소스 코드 기준으로 파일 별로 꼭 구현해야 하는 함수는 다음과 같으며, 전부 extern "C" 스코프 내에 정의되어 C++ 소스 코드의 함수라도 C언어 스타일로 실행되어야 한다. 해당하는 파일은 전부 소스 코드 리포지토리의 cl_dll 폴더에 존재한다.

  • cdll_int.cpp
 int		DLLEXPORT Initialize( cl_enginefunc_t *pEnginefuncs, int iVersion );
 int		DLLEXPORT HUD_VidInit( void );
 void	DLLEXPORT HUD_Init( void );
 int		DLLEXPORT HUD_Redraw( float flTime, int intermission );
 int		DLLEXPORT HUD_UpdateClientData( client_data_t *cdata, float flTime );
 void	DLLEXPORT HUD_Reset ( void );
 void	DLLEXPORT HUD_PlayerMove( struct playermove_s *ppmove, int server );
 void	DLLEXPORT HUD_PlayerMoveInit( struct playermove_s *ppmove );
 char	DLLEXPORT HUD_PlayerMoveTexture( char *name );
 int		DLLEXPORT HUD_ConnectionlessPacket( const struct netadr_s *net_from, const char *args, char *response_buffer, int *response_buffer_size );
 int		DLLEXPORT HUD_GetHullBounds( int hullnumber, float *mins, float *maxs );
 void	DLLEXPORT HUD_Frame( double time );
 void	DLLEXPORT HUD_VoiceStatus(int entindex, qboolean bTalking);
 void	DLLEXPORT HUD_DirectorMessage( int iSize, void *pbuf );
 void DLLEXPORT HUD_MobilityInterface( mobile_engfuncs_t *gpMobileEngfuncs );
  • demo.cpp
 void DLLEXPORT Demo_ReadBuffer( int size, unsigned char *buffer );
  • entity.cpp
 int DLLEXPORT HUD_AddEntity( int type, struct cl_entity_s *ent, const char *modelname );
 void DLLEXPORT HUD_CreateEntities( void );
 void DLLEXPORT HUD_StudioEvent( const struct mstudioevent_s *event, const struct cl_entity_s *entity );
 void DLLEXPORT HUD_TxferLocalOverrides( struct entity_state_s *state, const struct clientdata_s *client );
 void DLLEXPORT HUD_ProcessPlayerState( struct entity_state_s *dst, const struct entity_state_s *src );
 void DLLEXPORT HUD_TxferPredictionData ( struct entity_state_s *ps, const struct entity_state_s *pps, struct clientdata_s *pcd, const struct clientdata_s *ppcd, 
 struct weapon_data_s *wd, const struct weapon_data_s *pwd );
 void DLLEXPORT HUD_TempEntUpdate( double frametime, double client_time, double cl_gravity, struct tempent_s **ppTempEntFree, struct tempent_s **ppTempEntActive, int ( *Callback_AddVisibleEntity )( struct cl_entity_s *pEntity ), void ( *Callback_TempEntPlaySound )( struct tempent_s *pTemp, float damp ) );
 struct cl_entity_s DLLEXPORT *HUD_GetUserEntity( int index );
  • GameStudioModelRenderer.cpp
 int DLLEXPORT HUD_GetStudioModelInterface( int version, struct r_studio_interface_s **ppinterface, struct engine_studio_api_s *pstudio );
  • hl 폴더의 hl_weapons.cpp
 void _DLLEXPORT HUD_PostRunCmd( struct local_state_s *from, struct local_state_s *to, struct usercmd_s *cmd, int runfuncs, double time, unsigned int random_seed );
  • input.cpp
 struct kbutton_s DLLEXPORT *KB_Find( const char *name );
 void DLLEXPORT CL_CreateMove( float frametime, struct usercmd_s *cmd, int active );
 void DLLEXPORT HUD_Shutdown( void );
 int DLLEXPORT HUD_Key_Event( int eventcode, int keynum, const char *pszCurrentBinding );
  • input_mouse.cpp
 extern "C"  void DLLEXPORT IN_ClientMoveEvent( float forwardmove, float sidemove );
 extern "C" void DLLEXPORT IN_ClientLookEvent( float relyaw, float relpitch );
 extern "C" void DLLEXPORT IN_MouseEvent( int mstate );
 extern "C" void DLLEXPORT IN_ClearStates( void );
 extern "C" void DLLEXPORT IN_ActivateMouse( void );
 extern "C" void DLLEXPORT IN_DeactivateMouse( void );
 extern "C" void DLLEXPORT IN_Accumulate( void );
  • in_camera.cpp
 void DLLEXPORT CAM_Think( void );
 int DLLEXPORT CL_IsThirdPerson( void );
 void DLLEXPORT CL_CameraOffset( float *ofs );
  • tri.cpp
 void DLLEXPORT HUD_DrawNormalTriangles( void );
 void DLLEXPORT HUD_DrawTransparentTriangles( void );
  • view.cpp
 void DLLEXPORT V_CalcRefdef( struct ref_params_s *pparams );

서버(server.js)[편집 | 원본 편집]

먼저, 서버 측 코드에서는 다음 코드만 엔진이 읽는다. 이 함수는 서버 측에서 게임으로 엔진 기능을 가져오도록 구현해야 하는 함수다.

extern "C" void DLLEXPORT EXPORT2 GiveFnptrsToDll( enginefuncs_t *pengfuncsFromEngine, globalvars_t *pGlobals );

그러나 이와 별도로, eiface.h 헤더에 정의된 DLL_FUNCTIONS 구조체의 정적 변수(static 변수)로 선언하여 서버 측의 게임 로직을 매칭해야 한다. dlls 폴더의 cbase.cpp에 다음 형태로 정의되었다.

static DLL_FUNCTIONS gFunctionTable =
{
	GameDLLInit,				//pfnGameInit
	DispatchSpawn,				//pfnSpawn
	DispatchThink,				//pfnThink
	DispatchUse,				//pfnUse
	DispatchTouch,				//pfnTouch
	DispatchBlocked,			//pfnBlocked
	DispatchKeyValue,			//pfnKeyValue
	DispatchSave,				//pfnSave
	DispatchRestore,			//pfnRestore
	DispatchObjectCollsionBox,	//pfnAbsBox

	SaveWriteFields,			//pfnSaveWriteFields
	SaveReadFields,				//pfnSaveReadFields

	SaveGlobalState,			//pfnSaveGlobalState
	RestoreGlobalState,			//pfnRestoreGlobalState
	ResetGlobalState,			//pfnResetGlobalState

	ClientConnect,				//pfnClientConnect
	ClientDisconnect,			//pfnClientDisconnect
	ClientKill,					//pfnClientKill
	ClientPutInServer,			//pfnClientPutInServer
	ClientCommand,				//pfnClientCommand
	ClientUserInfoChanged,		//pfnClientUserInfoChanged
	ServerActivate,				//pfnServerActivate
	ServerDeactivate,			//pfnServerDeactivate

	PlayerPreThink,				//pfnPlayerPreThink
	PlayerPostThink,			//pfnPlayerPostThink

	StartFrame,					//pfnStartFrame
	ParmsNewLevel,				//pfnParmsNewLevel
	ParmsChangeLevel,			//pfnParmsChangeLevel

	GetGameDescription,         //pfnGetGameDescription    Returns string describing current .dll game.
	PlayerCustomization,        //pfnPlayerCustomization   Notifies .dll of new customization for player.

	SpectatorConnect,			//pfnSpectatorConnect      Called when spectator joins server
	SpectatorDisconnect,        //pfnSpectatorDisconnect   Called when spectator leaves the server
	SpectatorThink,				//pfnSpectatorThink        Called when spectator sends a command packet (usercmd_t)

	Sys_Error,					//pfnSys_Error				Called when engine has encountered an error

	PM_Move,					//pfnPM_Move
	PM_Init,					//pfnPM_Init				Server version of player movement initialization
	PM_FindTextureType,			//pfnPM_FindTextureType

	SetupVisibility,			//pfnSetupVisibility        Set up PVS and PAS for networking for this client
	UpdateClientData,			//pfnUpdateClientData       Set up data sent only to specific client
	AddToFullPack,				//pfnAddToFullPack
	CreateBaseline,				//pfnCreateBaseline			Tweak entity baseline for network encoding, allows setup of player baselines, too.
	RegisterEncoders,			//pfnRegisterEncoders		Callbacks for network encoding
	GetWeaponData,				//pfnGetWeaponData
	CmdStart,					//pfnCmdStart
	CmdEnd,						//pfnCmdEnd
	ConnectionlessPacket,		//pfnConnectionlessPacket
	GetHullBounds,				//pfnGetHullBounds
	CreateInstancedBaselines,   //pfnCreateInstancedBaselines
	InconsistentFile,			//pfnInconsistentFile
	AllowLagCompensation,		//pfnAllowLagCompensation
};

구조체 안에 있는 것은 전부 하프라이프의 서버 측 함수 코드다. 저걸 다 구현해야 비로소 게임의 실행이 가능하다.

물론 몇 개는 소스 코드의 pm_shared 폴더에 있는 C언어 파일들을 사용한다. PM_Move, PM_Init, PM_FindTextureType이 그것들이다.

여기까지는 게임 코드에서 다루는 것이고, 이제 Xash3D 엔진을 포팅하는 과정에 있는 장애물들을 알아보자

Xash3D 모듈[편집 | 원본 편집]

wrapper(index.js)[편집 | 원본 편집]

  • mod.js를 리버티게임 내에서 정의하면 인식하도록 script 태그를 페이지에 JavaScript로 추가
    • 이 경우 client-(게임 이름).js를 클라이언트 상호작용 스크립트(하프라이프 웹 포트의 client.js의 역할)로 가져와야 함

engine(xash.js)[편집 | 원본 편집]

현재 이슈[편집 | 원본 편집]

  • 현재 Multiplayer가 작동하지 않음: 아마도 WebSocket 문제로 추정 + 포트 27015을 리플리케이팅 특화 코드를 사용할 용도로 서버에서 개방해야 하나?
  • Secure WebSocket Connection 필요: 리플리케이터(replicator.js) 작성 후 microndk/build-emscripten-module.mk에서 WEBSOCKET_URL을 "wss://libertyga.me/xash3d/replicator.js" 아래로 설정하고 테스트할 것
  • 위키 사용자 명칭을 가져오지 못함: Console을 다시 활성화하고 playername 확인 필요 - Yes check.svg완료
    • 한글 닉네임을 인식하고 출력하지 못함: 유니코드 라이브러리를 같이 컴파일 해야함
  • 소스 엔진 애셋을 직접 가져올 수 없음: VTFLib 결합 필요
    • VTFLib는 엔비디아가 만든 구형 DXT 라이브러리를 쓰기 때문에 Libsquish를 사용하도록 개조 필요
  • 게임패드를 인식하지 못함: 브라우저 Gamepad API 관련 MDN 문서를 참조해 수정
  • quit 명령어 제거: 쓸모 없으며 에러만 일으킴

mainui(menu.js)[편집 | 원본 편집]

현재 이슈[편집 | 원본 편집]

  • Multiplayer 항목 진입시 사용자 명칭을 계속 물어봄: engine에서 인식하는데도 이러면 mainui 내 코드를 고쳐서 뜨지 않게 할 것 - Yes check.svg완료
    • Multiplayer 리플리케이터가 완성될 때까지 Main UI에서 Multiplayer 항목 비활성화

추가 모듈[편집 | 원본 편집]

  • 리플리케이션만 전문으로 하는 라우팅 서버용 리플리케이터 js 파일(replicator.js)을 제작
    • UDP 통신이 기본
    • 웹소켓으로 접속시 다음과 같은 통신을 함
      • 클라이언트 -> 서버: 라우팅 서버에 접속 시 모드 ID와 서버 ID 값을 저장하고 해당 유저가 서버에 데이터를 보낼 때 해당하는 서버와 통신을 주선함
      • 서버 -> 클라이언트: 서버의 통신 요청 시 모드 ID와 서버 ID 값이 같은 유저들을 찾아 전부 리플리케이트하고, 1대1 통신(TCP)의 경우에만 해당 유저를 찾아 통신을 중계
    • 위키 사이트 서버에 전용 서버를 설치할 수 없으므로 이런 조치를 취함