CSEngine은 유저가 작성할 수 있는 커스텀 스크립트를 지원합니다.
Squirrel Script를 사용하며, 스크립트의 개념은 유니티의 MonoBehaviour와 비슷합니다.
제가 엔진을 만들면서 정말 재밌었던 파트 중 하나입니다.
원래 Lua를 이용하여 커스텀 스크립트를 바인딩 할 예정이였으나, Lua와 vm로직이 비슷하고 문법의 기본적인 표준을 따르는 Squirrel Script를 바인딩하기로 결정하였습니다.
일단 기본적으로 주어지는 상태 관련 함수를
Init()
과 Tick()
으로 설정하였고, 호출 가능한 함수도 기본적으로 지원하는 기능들만 구현하였습니다.사실 여기까지만 해도 포럼에 관련 내용들이 크게 없어서 좀 많이 삽질도 하긴 했지만
제가 만든 엔진의 컴파일러 없이 게임의 스크립트를 바꾸는 것이 너무 신기해서 결과에 크게 만족하였습니다.
엔진에서의 커스텀 컴포넌트
유니티와 언리얼의 커스텀 컴포넌트는 위 이미지와 같이 구성되어있습니다.
그리고 사용자의 수정 및 추가가 가능한 범위는
유니티가 사용자 로직 단계, 언리얼이 블루프린트 단계와 추가적인 모듈 단계에서도 적용되고 있습니다.
CSEngine의 커스텀 컴포넌트도 언리얼과 비슷하게 수정 및 추가가 가능한 범위가 2가지로 존재합니다.
- 사용자 로직 (스크립트)
- 커스텀 게임 모듈 (C++)
사용자 로직 (스크립트)
커스텀 스크립트는 아래와 같은 방식으로 바인딩되어 VM을 통해 호출됩니다.
아래의 코드는 로직을 돌 때 중요한 파트들만 적어놨습니다.
클래스의 구조는 소스코드를 참조해주시면 감사하겠습니다.
- 기존 게임 모듈 단계의 클래스를 스크립트에 바인딩
//GameObject SQRClassDef<SGameObject>(_SC("GameObject"), vm) .Func(_SC("Find"), &SGameObject::Find) .Func(_SC("GetTransform"), &SGameObject::GetTransform) ... // 생략 .SquirrelFunc(_SC("GetClass"), GetCustomComponentFunc); //Component COMPONENT_DEF_WITH_SQNAME(CustomComponent, CSEngineComponent) .Func(_SC("GetGameObject"), &CustomComponent::GetGameObject) ... // 생략 //Util SQRClassDef<vec2>(_SC("vec2")) .Var(_SC("x"), &vec2::x) .Var(_SC("y"), &vec2::y) .Func(_SC("Set"), &vec2::Set); ... // 생략
- 에셋의 스크립트 문자열을 컴파일
HSQUIRRELVM vm = DefaultVM::Get(); //register script if (!script.empty()) { Script compiledScript; compiledScript.CompileString(script.c_str()); // 스크립트 문자열 컴파일 if (Sqrat::Error::Occurred(vm)) { #ifdef WIN32 OutputDebugString(_SC("Compile Failed: ")); OutputDebugString(Error::Message(vm).c_str()); #elif __ANDROID__ LOGE("Compile Failed : %s", Error::Message(vm).c_str()); #elif __linux__ std::cout << "Compile Failed : " << Error::Message(vm) << '\n'; #endif } // VM 상에 유효한 스크립트로 실행 compiledScript.Run(); if (Sqrat::Error::Occurred(vm)) { #ifdef WIN32 OutputDebugString(_SC("Run Failed: ")); OutputDebugString(Error::Message(vm).c_str()); #elif __ANDROID__ LOGE("Run Failed : %s", Error::Message(vm).c_str()); #elif __linux__ std::cout << "Run Failed : " << Error::Message(vm) << '\n'; #endif } // 실행이 끝나면 스크립트 오브젝트 릴리즈 compiledScript.Release(); }
- 컴포넌트 사이클을 통해 특정 상태의 함수를 호출
void CustomComponent::Tick(float elapsedTime) { // 스크립트 바인딩 관련 예외처리들 if (m_specialization == nullptr) return; if (m_classInstance == nullptr) return; if (m_funcTick < 0) return; if (m_isError) return; try { // 스크립트 내 Tick() 함수 호출 m_classInstance->call(m_funcTick, elapsedTime); } catch (Sqrat::Exception e) { m_isError = true; SafeLog::Log((Sqrat::LastErrorString(Sqrat::DefaultVM::Get()) + '\n').c_str()); } }
커스텀 게임 모듈 (C++)
현재 엔진을 라이브러리로 독립시키기엔 미구현 요소가 좀 많아 C++로 커스텀 컴포넌트를 추가하면 엔진에 종속되는 상태입니다.
일반적인 컴포넌트 사이클과 똑같으므로 코드는 컴포넌트 관련 소스코드를 통해 확인 가능합니다.