커스텀 스크립트

커스텀 스크립트

키워드
Squirrel Script
런타임 컴파일링
 
 
CSEngine은 유저가 작성할 수 있는 커스텀 스크립트를 지원합니다.
Squirrel Script를 사용하며, 스크립트의 개념은 유니티의 MonoBehaviour와 비슷합니다.
테스트용 커스텀 스크립트 일부분
테스트용 커스텀 스크립트 일부분
제가 엔진을 만들면서 정말 재밌었던 파트 중 하나입니다.
원래 Lua를 이용하여 커스텀 스크립트를 바인딩 할 예정이였으나, Lua와 vm로직이 비슷하고 문법의 기본적인 표준을 따르는 Squirrel Script를 바인딩하기로 결정하였습니다.
다른 오브젝트의 다른 커스텀 컴포넌트를 받아오는 예제입니다.
옆의 결과 이미지처럼 서로 같은 방향으로 돌아가고 있습니다.
다른 오브젝트의 다른 커스텀 컴포넌트를 받아오는 예제입니다. 옆의 결과 이미지처럼 서로 같은 방향으로 돌아가고 있습니다.
notion image
일단 기본적으로 주어지는 상태 관련 함수를 Init()Tick()으로 설정하였고, 호출 가능한 함수도 기본적으로 지원하는 기능들만 구현하였습니다.
사실 여기까지만 해도 포럼에 관련 내용들이 크게 없어서 좀 많이 삽질도 하긴 했지만 제가 만든 엔진의 컴파일러 없이 게임의 스크립트를 바꾸는 것이 너무 신기해서 결과에 크게 만족하였습니다.

엔진에서의 커스텀 컴포넌트

유니티와 언리얼의 커스텀 컴포넌트 구조
유니티와 언리얼의 커스텀 컴포넌트 구조
유니티와 언리얼의 커스텀 컴포넌트는 위 이미지와 같이 구성되어있습니다.
그리고 사용자의 수정 및 추가가 가능한 범위는 유니티가 사용자 로직 단계, 언리얼이 블루프린트 단계와 추가적인 모듈 단계에서도 적용되고 있습니다.
CSEngine의 커스텀 컴포넌트 구조
CSEngine의 커스텀 컴포넌트 구조
CSEngine의 커스텀 컴포넌트도 언리얼과 비슷하게 수정 및 추가가 가능한 범위가 2가지로 존재합니다.
  1. 사용자 로직 (스크립트)
  1. 커스텀 게임 모듈 (C++)

사용자 로직 (스크립트)

커스텀 스크립트는 아래와 같은 방식으로 바인딩되어 VM을 통해 호출됩니다.
💡
아래의 코드는 로직을 돌 때 중요한 파트들만 적어놨습니다. 클래스의 구조는 소스코드를 참조해주시면 감사하겠습니다.
  1. 기존 게임 모듈 단계의 클래스를 스크립트에 바인딩
    1. //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); ... // 생략
  1. 에셋의 스크립트 문자열을 컴파일
    1. 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(); }
  1. 컴포넌트 사이클을 통해 특정 상태의 함수를 호출
    1. 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++로 커스텀 컴포넌트를 추가하면 엔진에 종속되는 상태입니다.
일반적인 컴포넌트 사이클과 똑같으므로 코드는 컴포넌트 관련 소스코드를 통해 확인 가능합니다.