들어가는 말


변수들

sol로 변수들을 다루는 것은 매우 쉽고, 이전에 다뤄봤을 법한 어떤 연관 배열(associative array)이나 맵(map) 구조와 거의 동일하게 동작합니다.


읽기 (reading)

다음과 같은 lua 파일이 sol로 불러와진다고 합시다:

config = {
	fullscreen = false,
	resolution = { x = 1024, y = 768 }
}

Lua 가상 머신(Lua Virtual Machine)과는 다음과 같이 상호작용할 수 있습니다:

#define SOL_ALL_SAFETIES_ON 1
#include <sol/sol.hpp>
 
#include <tuple>
#include <assert.hpp>
#include <utility> // for std::pair
 
int main() {
 
	sol::state lua;
	lua.script_file("variables.lua");
	// "sol::state" 타입은 
	// 정확히 테이블처럼 동작합니다!
	bool isfullscreen = lua["config"]["fullscreen"]; // 중첩된 변수도 얻을 수 있음
	sol::table config = lua["config"];
	c_assert(!isfullscreen);
	return 0;
}

이 예시에서 보듯, 원하는 변수를 꺼내는 방법은 여러 가지입니다.
예를 들어, 중첩된 변수가 존재하는지 확인하기 위해 auto를 사용해 table[key] 조회의 값을 캡처하고 .valid() 메서드를 사용할 수 있습니다:

#define SOL_ALL_SAFETIES_ON 1
#include <sol/sol.hpp>
 
#include <tuple>
#include <assert.hpp>
#include <utility> // for std::pair
 
int main() {
 
	sol::state lua;
	lua.script_file("variables.lua");
 
	// test variable
	auto bark = lua["config"]["bark"];
	if (bark.valid()) {
		// 이 분기는 실행되지 않음: config 및/또는 bark는 변수가 아님
	}
	else {
		// 이 분기가 실행됨: config와 bark는 존재하는 변수임
	}
 
	return 0;
}

이 방법은 중첩된 변수가 존재하는지 확인할 때 유용합니다.
또한 최상위 변수가 존재하는지 확인하려면 sol::optional을 사용할 수 있는데, 이는
A) 접근하려는 키가 존재하는지, 그리고
B) 가져오려는 타입이 특정 타입인지
둘 다 확인해줍니다.


optional lookup

#define SOL_ALL_SAFETIES_ON 1
#include <sol/sol.hpp>
 
#include <tuple>
#include <assert.hpp>
#include <utility> // for std::pair
 
int main() {
 
	sol::state lua;
	lua.script_file("variables.lua");
 
	// optional도 사용 가능
	sol::optional<int> not_an_integer = lua["config"]["fullscreen"];
	if (not_an_integer) {
		// 이 분기는 실행되지 않음: 값이 정수가 아님
	}
 
	sol::optional<bool> is_a_boolean = lua["config"]["fullscreen"];
	if (is_a_boolean) {
		// 이 분기가 실행됨: 값이 boolean임
	}
 
	sol::optional<double> does_not_exist = lua["not_a_variable"];
	if (does_not_exist) {
		// 이 분기는 실행되지 않음: 해당 변수는 존재하지 않음
	}
	return 0;
}

이는 최적화 혹은 릴리스 모드에서도 안전성을 유지하며 검사를 수행할 때 유용합니다.
또한 특정 값이 존재할 수도 있지만 기본값으로 대체하고 싶을 때 get_or 메서드를 사용할 수 있습니다:


optional lookup (기본값)

#define SOL_ALL_SAFETIES_ON 1
#include <sol/sol.hpp>
 
#include <tuple>
#include <assert.hpp>
#include <utility> // for std::pair
 
int main() {
 
	sol::state lua;
	lua.script_file("variables.lua");
	// 이 결과는 '24'가 됩니다.
	// (숫자를 가져오려 하지만 fullscreen은 숫자가 아님)
	int is_defaulted = lua["config"]["fullscreen"].get_or(24);
	c_assert(is_defaulted == 24);
 
	// 이 결과는 config의 실제 값인 'false'가 됩니다.
	bool is_not_defaulted = lua["config"]["fullscreen"];
	c_assert(!is_not_defaulted);
 
	return 0;
}

이렇게 하면 변수를 읽는 작업이 끝입니다!


쓰기 (writing)

쓰기 작업은 훨씬 간단합니다.
파일이나 문자열을 스크립트로 실행하지 않아도, 마음대로 lua에 변수를 읽고 쓸 수 있습니다:

#define SOL_ALL_SAFETIES_ON 1
#include <sol/sol.hpp>
 
#include <iostream>
 
int main() {
 
	sol::state lua;
 
	// 기본 lua 라이브러리들을 열기 
	// (print()와 같은 기본 유틸리티용)
	lua.open_libraries(sol::lib::base);
 
	// 전역 테이블의 값
	lua["bark"] = 50;
 
	// 전역 테이블 안에 테이블 생성
	lua["some_table"] = lua.create_table_with(
		"key0", 24,
		"key1", 25,
		lua["bark"], "the key is 50 and this string is its value!");
 
	// 평범한 lua 코드 문자열 실행
	// C++에서 sol을 통해 설정한 것들을 lua에서도 접근할 수 있음
	// "Raw String Literal" 사용으로 여러 줄 문자열 가능:
	// http://en.cppreference.com/w/cpp/language/string_literal
	lua.script(R"(
		
	print(some_table[50])
	print(some_table["key0"])
	print(some_table["key1"])
 
	-- lua 주석: lua 스크립트에서 전역 변수를 _G 테이블로 접근
	print(_G["bark"])
 
	)");
 
	return 0;
}

이 예시는 가능한 대부분의 작업을 보여줍니다.
lua["non_existing_key_1"] = 1 구문은 그 변수를 생성하지만,
테이블을 먼저 만들지 않고 너무 깊이 들어가면 Lua API가 패닉을 일으킵니다
(예: lua["does_not_exist"]["b"] = 20 은 패닉 발생).

읽기/쓰기에서도 게으르게(lazy) 접근할 수 있습니다:

#define SOL_ALL_SAFETIES_ON 1
#include <sol/sol.hpp>
 
#include <iostream>
 
int main() {
 
	sol::state lua;
 
	auto barkkey = lua["bark"];
	if (barkkey.valid()) {
		// 실행되지 않음: 아직 존재하지 않음
		std::cout << "How did you get in here, arf?!" << std::endl;
	}
 
	barkkey = 50;
	if (barkkey.valid()) {
		// 실행됨: 값이 존재함!
		std::cout << "Bark Bjork Wan Wan Wan" << std::endl;
	}
 
	return 0;
}

마지막으로, C++에서 sol::lua_nil 상수를 사용해 참조/변수를 nil로 설정하여 삭제할 수도 있습니다:

#define SOL_ALL_SAFETIES_ON 1
#include <sol/sol.hpp>
 
#include <iostream>
 
int main() {
 
	sol::state lua;
	lua["bark"] = 50;
	sol::optional<int> x = lua["bark"];
	// x는 값을 가짐
	if (x) {
		std::cout << "x has no value, as expected" << std::endl;
	}
	else {
		return -1;
	}
 
	lua["bark"] = sol::lua_nil;
	sol::optional<int> y = lua["bark"];
	// y는 값을 가지지 않음
	if (y) {
		return -1;
	}
	else {
		std::cout << "y has no value, as expected" << std::endl;
	}
 
	return 0;
}

보시다시피 원하는 작업을 수행할 수 있는 방법은 다양합니다.
하지만, 지금까지는 단순한 숫자와 문자열만 다뤘습니다.
더 강력한 기능 — 즉, C++ 클래스나 함수 등을 넣고 싶다면 어떨까요?
이제 그걸 섞어봅시다!


functions and You

sol은 모든 종류의 함수를 등록할 수 있습니다. 빠른 예제(quick ‘n’ dirty)에서도 여러 예시가 나왔지만, 여기서는 sol로 감싸진 Lua 시스템에 함수를 등록할 수 있는 다양한 추가 방법들을 다룹니다.


새로운 함수 설정하기 (Setting a new function)

C++ 함수가 주어지면, 변수를 설정하는 것과 매우 비슷한 방식으로 여러 가지 방법으로 sol에 넣을 수 있습니다.

Registering C++ functions

#include <sol/sol.hpp>
 
std::string my_function( int a, std::string b ) {
        // 문자 'D'를 "a"번 반복한 문자열을 만들고,
        // 그것을 'b'에 이어붙임
        return b + std::string( 'D', a );
}
 
int main () {
 
        sol::state lua;
 
        lua["my_func"] = my_function; // 방법 1
        lua.set("my_func", my_function); // 방법 2
        lua.set_function("my_func", my_function); // 방법 3
 
        // 이 함수는 이제 'my_func'라는 이름으로
        // 이 상태(state)에서 실행되는 lua 스크립트 / 코드에서 접근 가능
        lua.script("some_str = my_func(1, 'Da')");
 
        // 방금 실행된 Lua 코드의 전역 변수 'some_str'을 읽어옴
        std::string some_str = lua["some_str"];
        // some_str == "DaD"
}

같은 코드가 클래스의 멤버 함수/멤버 변수 포인터, 람다(lambda) 함수에도 똑같이 동작합니다.


Registering C++ member functions

struct my_class {
        int a = 0;
 
        my_class(int x) : a(x) {
 
        }
 
        int func() {
                ++a; // a를 1 증가시킴
                return a;
        }
};
 
int main () {
 
        sol::state lua;
 
        // 여기서는 멤버 함수와 클래스 인스턴스를 바인딩함:
        // 지정된 클래스 인스턴스에서 함수를 호출하게 됨
        lua.set_function("my_class_func", &my_class::func, my_class());
 
        // 여기서는 클래스 인스턴스를 전달하지 않음:
        // Lua에서 이 함수를 호출하려면 "my_class"의 인스턴스를 직접 넘겨야 함
        lua.set_function("my_class_func_2", &my_class::func);
 
        // 사전 바인딩된 인스턴스 사용:
        lua.script(R"(
                first_value = my_class_func()
                second_value = my_class_func()
        )");
        // first_value == 1
        // second_value == 2
 
        // 인스턴스 미바인딩 버전 사용:
        lua.set("obj", my_class(24));
        // Lua에서 "obj"로 참조되는 클래스 인스턴스에 대해 "func" 호출
        lua.script(R"(
                third_value = my_class_func_2(obj)
                fourth_value = my_class_func_2(obj)
        )");
        // third_value == 25
        // fourth_value == 26
}

이런 식으로 설정하면 클래스의 멤버 함수와 멤버 변수는 모두 함수로 변환됩니다.
C++을 Lua에서 다룰 수 있도록 하는 usertype(사용자 정의 타입) 섹션을 배우면 obj.a = value 형태의 직관적인 변수 접근이 가능해지지만, 지금은 함수만 다루고 있습니다.


함수 템플릿에 대한 설명

많은 사람이 궁금해하는 점 중 하나가 함수 템플릿입니다.
템플릿 함수(멤버 함수이든 자유 함수이든)는 C++에서 인스턴스화(instantiation) 되기 전까지 존재하지 않기 때문에 그대로 등록할 수 없습니다.
즉, 아래와 같은 템플릿 함수가 있다면:

A C++ templated function

template <typename A, typename B>
auto my_add( A a, B b ) {
        return a + b;
}

모든 템플릿 인자를 명시적으로 지정해 주어야 바인딩할 수 있습니다.


Registering function template instantiations

int main () {
 
        sol::state lua;
 
        // 두 정수를 더함
        lua["my_int_add"] = my_add<int, int>;
 
        // 두 문자열을 연결함
        lua["my_string_combine"] = my_add<std::string, std::string>;
 
        lua.script("my_num = my_int_add(1, 2)");
        int my_num = lua["my_num"];
        // my_num == 3
 
        lua.script("my_str = my_string_combine('bark bark', ' woof woof')");
        std::string my_str = lua["my_str"];
        // my_str == "bark bark woof woof"
}

여기서 두 개의 별도 함수를 바인딩했습니다.
하지만 하나의 함수 이름으로 호출할 때 인자 타입에 따라 다르게 동작하게 만들고 싶다면 어떨까요?
이것을 오버로딩(Overloading) 이라고 하며, sol::overload를 이용해 다음과 같이 구현할 수 있습니다.


Registering C++ function template instantiations

int main () {
 
        sol::state lua;
 
        // 두 정수를 더하거나 두 문자열을 결합
        lua["my_combine"] = sol::overload( my_add<int, int>, my_add<std::string, std::string> );
 
        lua.script("my_num = my_combine(1, 2)");
        lua.script("my_str = my_combine('bark bark', ' woof woof')");
        int my_num = lua["my_num"];
        std::string my_str = lua["my_str"];
        // my_num == 3
        // my_str == "bark bark woof woof"
}

이 기능은 여러 타입을 받을 수 있고, 타입에 따라 다르게 동작해야 하는 함수에 유용합니다.
원한다면 여러 오버로드를 추가할 수 있으며, 타입이 다양해도 괜찮습니다.


💡 참고:
기본 인자를 가진 함수(default parameters)는 자동으로 여러 버전으로 바인딩되지 않습니다.
기본 인자를 지원하려면 직접 sol::overload를 사용해야 합니다.

또한, 람다나 호출 가능한 구조체(callable struct)를 바인딩할 때 어떤 식으로 동작하는지 잘 이해해야 합니다.


Lua로부터 함수 가져오기 (Getting a function from Lua)

Lua에서 함수를 가져오는 방법은 두 가지가 있습니다.
하나는 sol::function, 또 하나는 더 고급 버전인 sol::protected_function 입니다.

이 둘을 사용하면 Lua에서 정의된 callable을 가져와 C++에서 호출할 수 있습니다.


Retrieving a sol::function

int main () {
 
        sol::state lua;
 
        lua.script(R"(
                function f (a)
                        return a + 5
                end
        )");
 
        // 즉시 가져와서 바로 호출
        int x = lua["f"](30);
        // x == 35
 
        // 먼저 저장해두고 나중에 호출
        sol::function f = lua["f"];
        int y = f(20);
        // y == 25
}

set_function 등으로 C++에서 바인딩한 함수 역시 같은 방식으로 가져올 수 있습니다.

sol::protected_functionsol::function과 비슷하지만, 에러 처리 핸들러(error_handler) 를 설정할 수 있다는 점이 다릅니다.
이 핸들러는 모든 오류를 잡아 처리할 수 있습니다.


Retrieving a sol::protected_function

int main () {
        sol::state lua;
 
        lua.script(R"(
                function handler (message)
                        return "Handled this message: " .. message
                end
 
                function f (a)
                        if a < 0 then
                                error("negative number detected")
                        end
                        return a + 5
                end
        )");
 
        sol::protected_function f = lua["f"];
        f.error_handler = lua["handler"];
 
        sol::protected_function_result result = f(-500);
        if (result.valid()) {
                // 호출 성공
                int x = result;
        }
        else {
                // 호출 실패
                sol::error err = result;
                std::string what = err.what();
                // 'what'의 내용:
                // "Handled this message: negative number detected"
        }
}

Lua와의 다중 반환 (Multiple returns to and from Lua)

Lua와 C++ 사이에서 여러 값을 주고받을 때는
C++의 std::tuple 또는 std::pair 클래스를 사용하면 됩니다.
또는 sol::tie를 이용해 미리 선언한 변수들에 직접 할당할 수도 있습니다.


Multiple returns from Lua

int main () {
        sol::state lua;
 
        lua.script("function f (a, b, c) return a, b, c end");
 
        std::tuple<int, int, int> result;
        result = lua["f"](1, 2, 3);
        // result == { 1, 2, 3 }
 
        int a, b;
        std::string c;
        sol::tie( a, b, c ) = lua["f"](1, 2, "bark);
        // a == 1
        // b == 2
        // c == "bark"
}

Multiple returns into Lua

C++에서 Lua로 여러 값을 반환할 수도 있습니다.
예를 들어, C++의 람다를 Lua에 바인딩한 후, Lua에서 호출하면 std::tuple로 여러 값을 반환할 수 있습니다.

int main () {
        sol::state lua;
 
        lua["f"] = [](int a, int b, sol::object c) {
                // sol::object는 어떤 타입이든 가능: 그대로 전달
                return std::make_tuple( a, b, c );
        };
 
        std::tuple<int, int, int> result = lua["f"](1, 2, 3);
        // result == { 1, 2, 3 }
 
        std::tuple<int, int, std::string> result2;
        result2 = lua["f"](1, 2, "Arf?)
        // result2 == { 1, 2, "Arf?" }
 
        int a, b;
        std::string c;
        sol::tie( a, b, c ) = lua["f"](1, 2, "meow);
        // a == 1
        // b == 2
        // c == "meow"
}

여기서 sol::object는 Lua에서 들어오는 “아무 값(any value)”을 전달하기 위한 수단입니다.
또한 sol::make_object를 이용해 C++ 값으로부터 Lua 객체를 만들어 반환할 수도 있습니다.


Any return to and from Lua

(아무 타입이나 Lua로 주고받기)

앞의 예제에서도 언급했듯이, sol::object“아무 타입(any type)” 을 Lua로 전달할 때 유용합니다.
이는 C++ 표준에 std::variant<...> 가 완전히 도입될 때까지의 대안 역할을 합니다.

sol::objectsol::this_state와 함께 다음처럼 사용할 수 있습니다.


Return anything into Lua

sol::object fancy_func (sol::object a, sol::object b, sol::this_state s) {
        sol::state_view lua(s);
        if (a.is<int>() && b.is<int>()) {
                return sol::make_object(lua, a.as<int>() + b.as<int>());
        }
        else if (a.is<bool>()) {
                bool do_triple = a.as<bool>();
                return sol::make_object(lua, b.as<double>() * ( do_triple ? 3 : 1 ) );
        }
        return sol::make_object(lua, sol::lua_nil);
}
 
int main () {
        sol::state lua;
 
        lua["f"] = fancy_func;
 
        int result = lua["f"](1, 2);
        // result == 3
        double result2 = lua["f"](false, 2.5);
        // result2 == 2.5
 
        // Lua에서 호출, 결과 받기
        lua.script("result3 = f(true, 5.5)");
        double result3 = lua["result3"];
        // result3 == 16.5
}

이로써 함수(Functions) 와 sol 간의 상호작용에 대해 거의 모든 것을 다뤘습니다.
좀 더 고급 트릭이나 기능을 배우고 싶다면 sol::this_statesol::variadic_args를 참고하세요.
다음 튜토리얼 주제는 C++ 타입(usertypes)을 Lua에서 사용하는 방법입니다.
C++ 쪽 함수의 인자 활용법이나 효율적인 사용법이 더 궁금하다면 관련 노트를 참고하세요.


C++ in Lua

사용자 정의 타입(“usertype” 또는 간단히 “udt”)을 sol에서 사용하는 것은 간단합니다.
만약 멤버 변수나 함수를 호출하지 않는다면, 굳이 ‘등록(register)’할 필요도 없습니다 — 그냥 그대로 전달하면 됩니다.
하지만 Lua 안에서 usertype의 변수와 함수를 사용하려면, 반드시 등록을 해야 합니다.

여기서는 여러 가지 정보를 포함한 짧은 예시를 통해 usertype을 다루는 방법을 보여드리겠습니다.


player.hpp

#include <iostream>
 
struct player {
public:
	int bullets;
	int speed;
 
	player()
		: player(3, 100) {
 
	}
 
	player(int ammo)
		: player(ammo, 100) {
 
	}
 
	player(int ammo, int hitpoints)
		: bullets(ammo), hp(hitpoints) {
 
	}
 
	void boost() {
		speed += 10;
	}
 
	bool shoot() {
		if (bullets < 1)
			return false;
		--bullets;
		return true;
	}
 
	void set_hp(int value) {
		hp = value;
	}
 
	int get_hp() const {
		return hp;
	}
 
private:
	int hp;
};

이 클래스는 비교적 단순하지만, Lua에서 메타테이블(metatable)을 직접 작성하는 일은 피하고 싶습니다.
우리는 이 클래스를 Lua에 손쉽게 통합하고 싶습니다.


우리가 Lua에서 사용하고 싶은 코드 (player_script.lua)

p1 = player.new(2)
 
-- p2는 아래에서 lua["p2"] = player(0); 으로 설정되어 있으므로 여전히 존재함
local p2shoots = p2:shoot()
assert(not p2shoots)
-- 탄약이 0이었음
 
-- 변수 프로퍼티 설정자(setter)
p1.hp = 545
-- 프로퍼티 접근자(unqualified getter)로 값 얻기
print(p1.hp)
assert(p1.hp == 545)
 
local did_shoot_1 = p1:shoot()
print(did_shoot_1)
print(p1.bullets)
local did_shoot_2 = p1:shoot()
print(did_shoot_2)
print(p1.bullets)
local did_shoot_3 = p1:shoot()
print(did_shoot_3)
 
-- 읽기 가능
print(p1.bullets)
-- 아래는 오류 발생: bullets는 읽기 전용 변수이므로 쓸 수 없음
-- p1.bullets = 20
 
p1:boost()
-- Lua 스크립트에서 런타임에 정의한 함수 호출
p1:brake()

이 동작을 가능하게 하려면, new_usertypemethod를 사용해 바인딩해야 합니다.
이 메서드들은 tablestate(_view) 양쪽 모두에 존재하지만, 여기서는 state를 사용하겠습니다.


main.cpp

#define SOL_ALL_SAFETIES_ON 1
#include <sol/sol.hpp>
 
#include "player.hpp"
 
#include <iostream>
 
int main() {
	sol::state lua;
 
	lua.open_libraries(sol::lib::base);
 
	// usertype을 등록하기 전에 userdata를 설정해도 괜찮음.
	// 이후에 usertype을 등록하면 올바른 메타테이블이 자동으로 연결됨.
 
	// "p2"라는 변수를 player 타입(탄약 0)으로 설정
	lua["p2"] = player(0);
 
	// usertype 메타테이블 생성
	sol::usertype<player> player_type = lua.new_usertype<player>("player",
		// 3개의 생성자
		sol::constructors<player(), player(int), player(int, int)>());
 
	// 값을 반환하는 일반적인 멤버 함수
	player_type["shoot"] = &player::shoot;
	// 일반적인 멤버 함수
	player_type["boost"] = &player::boost;
 
	// 멤버 변수처럼 get/set 가능한 프로퍼티
	player_type["hp"] = sol::property(&player::get_hp, &player::set_hp);
 
	// 읽기/쓰기 가능한 변수
	player_type["speed"] = &player::speed;
	// 읽기 전용 변수
	// .set(foo, bar)는 [foo] = bar 와 동일
	player_type.set("bullets", sol::readonly(&player::bullets));
 
	lua.script_file("prelude_script.lua");
	lua.script_file("player_script.lua");
	return 0;
}

추가 Lua 코드 (prelude_script.lua)

function player:brake ()
	self.speed = 0
	print("we hit the brakes!")
end

이 스크립트는 C++에서 정의되지 않은 brake 메서드를 Lua 쪽에서 동적으로 추가하는 예시입니다.


이제 이 스크립트를 실행하면 잘 작동할 것입니다.
값의 변화를 관찰하거나 실험도 가능하죠.

이 밖에도 다음과 같은 고급 기능들이 존재합니다:

  • 초기화 함수 (private 생성자 / 소멸자 지원)

  • name.my_function(...) 형태로 호출 가능한 “정적(static)” 함수

  • 오버로딩된 멤버 함수

  • sol::var을 이용한 전역 변수 바인딩 (std::ref를 사용하면 참조로도 가능)


이 방법은 단순히 함수를 등록하는 수준을 넘어,
Lua에서 C++ 코드를 재활용할 수 있는 강력한 방법입니다.

더 복잡한 클래스나 자료 구조를 Lua에 노출할 수 있으며,
만약 usertype 이상의 세밀한 제어가 필요하다면
sol의 커스터마이징 및 확장 기능을 통해 원하는 동작을 정의할 수도 있습니다.

더 많은 예시와 복잡한 코드는 examples 디렉터리의
usertype_ 접두어가 붙은 예시들을 참고하면 됩니다.


소유권 (ownership)

C++에서 리소스를 관리할 때 소유권은 중요합니다.
sol은 여러 가지 소유권(ownership) 의미 체계를 가지고 있으며, 대부분 기본적으로 안전합니다.
아래는 그 규칙들입니다.


객체 소유권 (object ownership)

Lua에 존재하는 어떤 것에 대한 참조를 가져오려면 sol::reference 또는 sol::object를 통해 가져올 수 있습니다:

object_lifetime.cpp

#define SOL_ALL_SAFETIES_ON 1
#include <sol/sol.hpp>
 
#include <string>
#include <iostream>
 
int main () {
	sol::state lua;
	lua.open_libraries(sol::lib::base);
 
	lua.script(R"(
	obj = "please don't let me die";
	)");
 
	sol::object keep_alive = lua["obj"];
	lua.script(R"(
	obj = nil;
	function say(msg)
		print(msg)
	end
	)");
 
	lua.collect_garbage();
 
	lua["say"](lua["obj"]);
	// 여전히 여기서 접근 가능하며 Lua 내에서도 살아 있음
	// 이름이 지워졌더라도
	std::string message = keep_alive.as<std::string>();
	std::cout << message << std::endl;
 
	// Lua에 다시 인자로 전달하거나
	// 새 이름으로 지정할 수도 있음
	// 원하는 대로!
	lua["say"](keep_alive);
 
	return 0;
}

모든 객체는 sol::state가 파괴되기 전에 반드시 파괴되어야 합니다.
그렇지 않으면 Lua 상태(Lua State)에 대한 dangling reference(잘못된 참조) 가 생겨
끔찍하고 무시무시한 방식으로 프로그램이 터질 것입니다. 💥

이 규칙은 단지 sol::object에만 해당되지 않습니다.
sol::referencesol::object에서 파생된 모든 타입 (sol::table, sol::userdata, 등등) 역시
state가 범위를 벗어나기 전에 정리(clean up) 되어야 합니다.


포인터 소유권 (pointer ownership)

solraw pointer (일반 포인터) 의 소유권을 가져가지 않습니다.
Raw pointer는 어떤 것도 소유하지 않기 때문입니다.

따라서 sol은 raw pointer를 삭제하지 않습니다.
(삭제하면 안 됩니다. 소유자가 아니기 때문입니다.)

pointer_lifetime.cpp

#define SOL_ALL_SAFETIES_ON 1
#include <sol/sol.hpp>
 
struct my_type {
	void stuff() {
	}
};
 
int main() {
 
	sol::state lua;
	// AAAHHH BAD
	// dangling pointer!
	lua["my_func"] = []() -> my_type* { return new my_type(); };
 
	// AAAHHH!
	lua.set("something", new my_type());
 
	// AAAAAAHHH!!!
	lua["something_else"] = new my_type();
	return 0;
}

대신 unique_ptr 또는 shared_ptr 을 사용하거나,
단순히 값(value) 자체를 반환하세요.

(스마트 포인터 사용) pointer_lifetime.cpp

#define SOL_ALL_SAFETIES_ON 1
#include <sol/sol.hpp>
 
struct my_type {
	void stuff() {
	}
};
 
int main() {
 
	sol::state lua;
	// :ok:
	lua["my_func0"] = []() -> std::unique_ptr<my_type> { return std::make_unique<my_type>(); };
 
	// :ok:
	lua["my_func1"] = []() -> std::shared_ptr<my_type> { return std::make_shared<my_type>(); };
 
	// :ok:
	lua["my_func2"] = []() -> my_type { return my_type(); };
 
	// :ok:
	lua.set("something", std::unique_ptr<my_type>(new my_type()));
 
	std::shared_ptr<my_type> my_shared = std::make_shared<my_type>();
	// :ok:
	lua.set("something_else", my_shared);
 
	// :ok:
	auto my_unique = std::make_unique<my_type>();
	lua["other_thing"] = std::move(my_unique);
 
	return 0;
}

만약 수명이 충분히 길어질 것임을 확실히 알고 있고,
Lua에 참조(reference) 로 넘기기만 하려는 것이라면
그것도 괜찮습니다:

(static) pointer_lifetime.cpp

#define SOL_ALL_SAFETIES_ON 1
#include <sol/sol.hpp>
 
struct my_type {
	void stuff() {
	}
};
 
int main() {
 
	sol::state lua;
	lua["my_func5"] = []() -> my_type* {
		static my_type mt;
		return &mt;
	};
	return 0;
}

solnullptr 을 감지할 수 있습니다.
따라서 반환할 때 nullptr이면,
dangling 참조 대신 sol::lua_nil 값이 푸시됩니다.

하지만 미리 nil임을 알고 있다면,
std::nullptr_t 또는 sol::lua_nil 을 명시적으로 반환하는 것이 좋습니다.

(nil / nullptr) pointer_lifetime.cpp

#define SOL_ALL_SAFETIES_ON 1
#include <sol/sol.hpp>
 
struct my_type {
	void stuff() {
	}
};
 
int main() {
 
	sol::state lua;
	// 이건 여전히 나쁨 DON'T DO IT AAAHHH BAD
	// 빈 unique_ptr을 반환하거나
	// 명시적으로 하세요!
	lua["my_func6"] = []() -> my_type* { return nullptr; };
 
	// :ok:
	lua["my_func7"] = []() -> std::nullptr_t { return nullptr; };
 
	// :ok:
	lua["my_func8"] = []() -> std::unique_ptr<my_type> {
		// 기본 생성되며 nullptr로 초기화됨
		// Lua로는 nil로 푸시됨
		return std::unique_ptr<my_type>();
		// std::shared_ptr도 동일하게 동작함
	};
 
	// 허용됨, 'something'을 nil로 설정함
	// (참조가 없으면 다음 GC 때 삭제됨)
	lua.set("something", nullptr);
 
	// 이것도 괜찮음
	lua["something_else"] = nullptr;
 
	return 0;
}

일시적 (ephemeral, proxy) 객체

Proxyresult 타입들은 일시적(ephemeral) 입니다.
이들은 Lua 스택에 의존하며, 생성자와 소멸자가 Lua 스택과 상호작용합니다.

즉, 이런 객체들은 C++ 함수에서 반환하기에 매우 위험합니다.
(매우 신중한 관리 없이는 스택이 해제된 후 참조 오류가 발생합니다.)

다음과 같은 스택 기반 객체들을 사용할 때 주의해야 합니다:

  • protected_function_result

  • function_result

  • load_result

  • stack_reference

  • 그 밖의 Lua 스택을 직접 다루는 타입들

이런 것들을 반환하고 싶다면, 다시 생각해보세요.
특히 여러 개의 load/function 결과를 한 C++ 함수에서 처리하는 경우
구현 세부사항(implementation-defined behavior)에 의존해야 하므로
매우 위험합니다.


정리 요약

  • sol::object, sol::table 등은 Lua state보다 오래 살아서는 안 됨

  • raw pointer 사용 금지, 대신 unique_ptr / shared_ptr / 값 사용

  • nullptr 반환 시 nil로 처리됨

  • 스택 기반 객체는 반환하지 말 것


자신만의 타입 추가하기 (Adding your own types)

가끔은 sol이 특정 구조체(struct)나 클래스(class)를 단순히 userdata로 처리하는 것 대신,
다른 방식으로 다루도록 직접 오버라이드(override) 하고 싶을 때가 있습니다.

이를 위해서는 sol이 제공하는 4가지 커스터마이즈 지점(customization point) 을 활용해야 합니다.
이들은 다음과 같습니다:

  • sol_lua_check

  • sol_lua_get

  • sol_lua_push

  • sol_lua_check_get

이들은 템플릿 클래스/구조체이므로, C++에서 특수화(specialization) 를 통해 오버라이드합니다.

아래는 C++ → Lua 방향으로 갈 때 구조체를 두 개의 값으로 분해하고,
Lua → C++으로 돌아올 때 다시 하나의 구조체로 재조합하는 예시입니다:


main.cpp

#define SOL_ALL_SAFETIES_ON 1
#include <sol/sol.hpp>
 
#include <iostream>
#include "assert.hpp"
 
struct two_things {
	int a;
	bool b;
};
 
template <typename Handler>
bool sol_lua_check(sol::types<two_things>, lua_State* L, int index, Handler&& handler, sol::stack::record& tracking) {
	// 인덱스는 음수일 수 있으며, 이는 스택의 위에서부터 카운트합니다.
	// 이를 처리하기 위해 lua_absindex 함수를 이용해 절대 인덱스로 변환합니다.
	int absolute_index = lua_absindex(L, index);
	// 첫 번째와 두 번째 인덱스가 올바른 타입인지 검사합니다.
	bool success = sol::stack::check<int>(L, absolute_index, handler) && sol::stack::check<bool>(L, absolute_index + 1, handler);
	tracking.use(2);
	return success;
}
 
two_things sol_lua_get(sol::types<two_things>, lua_State* L, int index, sol::stack::record& tracking) {
	int absolute_index = lua_absindex(L, index);
	// 첫 번째 요소를 가져옵니다.
	int a = sol::stack::get<int>(L, absolute_index);
	// 두 번째 요소는 첫 번째로부터 +1 위치에서 가져옵니다.
	bool b = sol::stack::get<bool>(L, absolute_index + 1);
	// 우리는 2개의 슬롯을 사용합니다 (int 1개, bool 1개)
	tracking.use(2);
	return two_things { a, b };
}
 
int sol_lua_push(lua_State* L, const two_things& things) {
	int amount = sol::stack::push(L, things.a);
	// amount는 1입니다: int는 스택에 1개의 항목을 푸시함
	amount += sol::stack::push(L, things.b);
	// amount는 2가 됩니다: bool도 1개의 항목을 푸시함
	// 두 가지 값을 반환
	return amount;
}
 
int main() {
	std::cout << "=== customization ===" << std::endl;
	std::cout << std::boolalpha;
 
	sol::state lua;
	lua.open_libraries(sol::lib::base);

이것이 여러분이 자신만의 클래스로 확장할 때 따라야 할 기본 공식(base formula) 입니다.
이후 라이브러리의 다른 부분에서도 매끄럽게 사용할 수 있습니다.


main.cpp (계속)

	// 단순 pass-through 스타일의 함수를 생성합니다.
	lua.script("function f ( a, b ) print(a, b) return a, b end");
 
	// Lua에서 함수를 가져옵니다.
	sol::function f = lua["f"];
 
	two_things things = f(two_things { 24, false });
	c_assert(things.a == 24);
	c_assert(things.b == false);
	// things.a == 24
	// things.b == true
 
	std::cout << "things.a: " << things.a << std::endl;
	std::cout << "things.b: " << things.b << std::endl;
	std::cout << std::endl;
 
	return 0;
}

이것으로 끝입니다! 🎉


구현에 대한 몇 가지 주의사항

  1. sol::stack::record 타입의 보조 매개변수(auxiliary parameter)가
    getter와 checker 함수들에 사용됩니다.
    이는 마지막으로 완료된 작업을 추적하기 위한 것입니다.

    위 예시에서는 두 멤버를 가져왔으므로
    tracking.use(2); 를 호출해 2개의 스택 위치를 사용했음을 표시했습니다.
    (하나는 int, 하나는 bool)

  2. 또한 index 매개변수를 사용하고,
    그 다음 멤버를 위해 index + 1 을 사용한 점에 주목하세요.


만약 여러분이 어떤 타입을 Lua로 푸시(push) 할 수 있도록 만들고 싶지만
Lua에서 가져올(get) 필요는 없다면,
시스템의 한 부분만 특수화(specialization) 하면 됩니다.

  • Lua에서 값을 가져와야 하는 경우
    sol::stack::gettersol::stack::checker 템플릿 클래스를 특수화해야 합니다.

  • Lua로 값을 푸시해야 하는 경우
    sol::stack::pusher 템플릿 클래스를 특수화해야 합니다.

  • sol::lua_size 템플릿 클래스 특성(trait)도 두 경우에 대해 특수화해야 하지만,
    만약 한 개의 아이템만 푸시(push)한다면
    기본 구현(default implementation)이 자동으로 1을 가정합니다.


⚠️ 주의사항 (Note)

여기서 중요한 점은,
get, push, check 함수들은 T 타입과 T* (포인터 타입)를 구별한다는 것입니다.

즉, 만약 T와 동일하지 않은 의미 체계를 가진 T* 핸들을
독립적으로 다루고 싶다면,
T*T 둘 다에 대한 checker/getter/pusher 를 정의해야 할 수도 있습니다.

  • T*checkerT의 checker를 forward 하지만,

  • T*getterT의 getter로 forward되지 않습니다.
    (예: int*int와 다르기 때문입니다.)


일반적으로 대부분의 getter/checker는 스택의 한 지점만 사용하므로 문제가 없습니다.
하지만 복잡한 중첩 클래스(nested class) 를 다룬다면,
tracking.last 를 이용해
마지막 get 또는 check 작업이 몇 개의 스택 인덱스를 사용했는지 확인한 후,
stack::check<..>(L, index, tracking) 호출 뒤에
index + tracking.last 로 증가시키는 것이 유용합니다.


더 자세한 내용은 stack 관련 API 페이지에서
확장 포인트(extension point)에 대한 설명을 확인할 수 있습니다.

또한 무언가 잘못되었거나 추가적인 질문이 있다면
GitHub Issues 페이지에 글을 남기거나 이메일로 문의해 주세요!


요약

  • sol_lua_check, sol_lua_get, sol_lua_push 등을 통해 사용자 정의 타입 변환 가능

  • sol::stack::record는 스택 사용량 추적용

  • TT*별개로 처리됨

  • getter/checker/pusher를 적절히 특수화(specialize) 하면
    C++ ↔ Lua 간 사용자 정의 타입을 자유롭게 주고받을 수 있음


GPT Chatting : sol2가 타입의 상속 지원 기능이 있는지


1️⃣ 기본 원칙

Sol2는 C++ 상속을 Lua 쪽에서도 인식할 수 있게 설계되어 있습니다.

  • 부모 클래스(Base)를 Lua에 먼저 등록

  • 자식 클래스(Derived)를 등록할 때 **sol::bases<Base>**를 지정하면 부모의 멤버/메서드를 자동으로 상속

즉, 부모 클래스에서 등록한 요소는 자식에서 다시 등록할 필요 없음.


2️⃣ 예시

#include <sol/sol.hpp>
#include <iostream>
 
class Base {
public:
    void Foo() { std::cout << "Base::Foo\n"; }
};
 
class Derived : public Base {
public:
    void Bar() { std::cout << "Derived::Bar\n"; }
};
 
int main() {
    sol::state lua;
    lua.open_libraries(sol::lib::base);
 
    // 부모 클래스 등록
    lua.new_usertype<Base>("Base",
        "Foo", &Base::Foo
    );
 
    // 자식 클래스 등록, bases<> 사용
    lua.new_usertype<Derived>("Derived",
        sol::bases<Base>(),  // Base 상속
        "Bar", &Derived::Bar
    );
 
    lua.script(R"(
        local d = Derived.new()
        d:Foo()  -- Base::Foo 호출
        d:Bar()  -- Derived::Bar 호출
    )");
 
    return 0;
}
  • Lua에서 Derived 객체는 자동으로 Base의 메서드(Foo)를 사용할 수 있음

  • Base에서 이미 등록한 Foo를 Derived에서 다시 등록할 필요 없음


3️⃣ 주의 사항

  1. 순서 중요: 부모 클래스 먼저 등록 → 자식 클래스 등록 시 sol::bases<Parent> 지정

  2. 부모 클래스가 포인터 멤버를 갖고 있으면, 해당 타입도 사전에 Lua에 등록되어 있어야 함

  3. 다중 상속도 가능:

class Derived2 : public Base, public AnotherBase {};
lua.new_usertype<Derived2>("Derived2", sol::bases<Base, AnotherBase>(), ...);
  1. 멤버 변수는 상속되지 않음 → Lua에서 직접 접근할 수 있는 공용 멤버만 상속됨

정리하면:

  • 메서드 상속: 부모에서 등록하면 자식에서 중복 등록 불필요

  • 멤버 변수 상속: public이면 Lua에서 접근 가능, private/protected는 getter/setter 필요

  • sol::bases<Parent>로 상속 관계를 명시해야 Lua에서도 자동 상속됨

Claude Chatting : lua에서 메타테이블로 Class 흉내내는 법

한 줄씩 뜯어서 설명드리겠습니다!

1️⃣ Enemy = {}

Enemy = {}
  • 빈 테이블을 생성하고 Enemy라는 전역 변수에 할당
  • 이 테이블이 “클래스”가 될 것임
  • 여기에 메서드와 속성을 추가할 예정
-- 내부적으로는 이렇게 동작
Enemy = {}
-- Enemy는 메모리 어딘가의 빈 테이블을 가리킴
-- 주소: 0x1234 -> {}

2️⃣ Enemy.__index = Enemy

Enemy.__index = Enemy
  • Enemy 테이블에 __index라는 특수 키를 추가
  • 값은 Enemy 자기 자신을 가리킴

왜 자기 자신을 가리키나?

-- 시각화
Enemy = {
    __index = Enemy,  -- 자기 자신을 가리킴 (순환 참조)
    -- 나중에 여기에 메서드들이 추가됨
}

이렇게 하면:

-- 나중에 인스턴스에서 메서드를 찾을 때:
instance.someMethod
-- 1. instance에 없음
-- 2. instance의 메타테이블(__index)을 확인 → Enemy를 가리킴
-- 3. Enemy.someMethod를 찾음

3️⃣ function Enemy.new()

function Enemy.new()

두 가지 표기법 비교:

-- 방법 1: 점(.) 표기법
function Enemy.new()
    -- self가 자동으로 전달되지 않음
end
 
-- 방법 2: 콜론(:) 표기법
function Enemy:new()
    -- self가 첫 번째 매개변수로 자동 전달됨
end

실제로는 이렇게 변환됨:

-- 이 코드는
function Enemy.new()
end
 
-- 이렇게 저장됨
Enemy["new"] = function()
end
 
-- 즉, Enemy 테이블에 "new"라는 키로 함수를 저장
Enemy = {
    __index = Enemy,
    new = function() ... end  -- 여기 추가됨
}

4️⃣ local self = setmetatable({}, Enemy)

가장 핵심적인 부분입니다!

local self = setmetatable({}, Enemy)

단계별 분해:

A. {}

{}  -- 새로운 빈 테이블 생성 (인스턴스가 될 것)

B. setmetatable(테이블, 메타테이블)

setmetatable({}, Enemy)
-- 첫 번째 인자: 빈 테이블 (인스턴스)
-- 두 번째 인자: Enemy (메타테이블로 사용될 테이블)

setmetatable이 하는 일:

-- 내부 동작 (실제로는 C로 구현됨)
instance = {}
instance의 숨겨진 메타테이블 링크 = Enemy
 
-- 시각화
instance = {
    -- 숨겨진 내부 속성
    __metatable__ = Enemy  -- (실제 키 이름은 다름)
}

C. local self

local self = setmetatable({}, Enemy)
-- 결과를 지역 변수 self에 할당
-- 이제 self는 Enemy를 메타테이블로 가진 인스턴스

5️⃣ self.hp = 100

self.hp = 100
  • self 테이블에 hp라는 키를 추가하고 값을 100으로 설정
  • 이건 일반적인 테이블 할당
-- 결과:
self = {
    hp = 100
    -- 메타테이블: Enemy
}

6️⃣ return self

return self
  • 만들어진 인스턴스를 반환
  • 호출자가 이 인스턴스를 받아서 사용

🔍 전체 흐름 시각화

-- 1단계: 클래스 정의
Enemy = {}                    -- 빈 테이블
Enemy.__index = Enemy         -- 메타메서드 설정
Enemy = {
    __index = Enemy  -- 순환 참조
}
 
-- 2단계: 생성자 추가
function Enemy.new()
    -- ...
end
 
Enemy = {
    __index = Enemy,
    new = function() ... end
}
 
-- 3단계: 메서드 추가
function Enemy:takeDamage(damage)
    self.hp = self.hp - damage
end
 
Enemy = {
    __index = Enemy,
    new = function() ... end,
    takeDamage = function(self, damage) ... end
}
 
-- 4단계: 인스턴스 생성
enemy1 = Enemy.new()
 
-- enemy1의 구조:
enemy1 = {
    hp = 100
    -- 메타테이블: Enemy
}
 
-- 5단계: 메서드 호출
enemy1:takeDamage(30)
 
-- 실제 동작:
-- 1. enemy1에 takeDamage가 있나? → 없음
-- 2. enemy1의 메타테이블(__index) 확인 → Enemy
-- 3. Enemy.takeDamage 찾음 → 있음!
-- 4. Enemy.takeDamage(enemy1, 30) 실행

📊 메모리 구조

메모리 레이아웃:

Enemy 테이블 (0x1000)
├─ __index -> 0x1000 (자기 자신)
├─ new -> function
└─ takeDamage -> function

enemy1 인스턴스 (0x2000)
├─ hp = 100
└─ [메타테이블] -> 0x1000 (Enemy)

enemy2 인스턴스 (0x3000)
├─ hp = 100
└─ [메타테이블] -> 0x1000 (Enemy)

→ 모든 인스턴스가 같은 Enemy를 공유
→ 메서드는 한 곳에만 저장 (메모리 효율적)

🎯 왜 이렇게 복잡하게?

직관적인 방법 (비효율적):

function Enemy.new()
    local self = {}
    self.hp = 100
    
    -- 메서드를 매번 복사 ❌
    self.takeDamage = function(damage)
        self.hp = self.hp - damage
    end
    
    return self
end
 
-- 문제: 인스턴스 100개면 함수도 100개 복사됨!

메타테이블 방법 (효율적):

function Enemy.new()
    local self = setmetatable({}, Enemy)
    self.hp = 100
    return self
end
 
-- 장점: 
-- - 메서드는 Enemy에 한 번만 저장
-- - 인스턴스는 데이터만 가짐
-- - 메모리 효율적!

이해가 되셨나요? 특정 부분을 더 설명해드릴까요?


Lua 클래스 템플릿을 만들어드릴게요! 복사해서 바로 사용하세요.

📋 기본 클래스 템플릿

-- ============================================
-- 클래스명: MyClass
-- 설명: 클래스에 대한 간단한 설명
-- ============================================
 
MyClass = {}
MyClass.__index = MyClass
 
-- 생성자
function MyClass.new(param1, param2)
    local self = setmetatable({}, MyClass)
    
    -- 속성 초기화
    self.property1 = param1
    self.property2 = param2
    
    return self
end
 
-- 메서드 (콜론 사용 - self 자동 전달)
function MyClass:method1()
    -- self.property1 사용 가능
end
 
function MyClass:method2(arg)
    -- 메서드 구현
end
 
-- 사용 예시:
-- local obj = MyClass.new("value1", "value2")
-- obj:method1()

📋 상속 클래스 템플릿

-- ============================================
-- 부모 클래스
-- ============================================
 
ParentClass = {}
ParentClass.__index = ParentClass
 
function ParentClass.new(baseValue)
    local self = setmetatable({}, ParentClass)
    self.baseValue = baseValue
    return self
end
 
function ParentClass:parentMethod()
    print("Parent method")
end
 
 
-- ============================================
-- 자식 클래스 (ParentClass 상속)
-- ============================================
 
ChildClass = setmetatable({}, {__index = ParentClass})
ChildClass.__index = ChildClass
 
function ChildClass.new(baseValue, childValue)
    -- 부모 생성자 호출
    local self = setmetatable(ParentClass.new(baseValue), ChildClass)
    
    -- 자식 속성 추가
    self.childValue = childValue
    
    return self
end
 
-- 부모 메서드 오버라이드
function ChildClass:parentMethod()
    print("Child method (overridden)")
end
 
-- 자식 전용 메서드
function ChildClass:childMethod()
    -- 부모 속성 접근 가능: self.baseValue
    -- 자식 속성 접근: self.childValue
end
 
-- 사용 예시:
-- local child = ChildClass.new(100, 200)
-- child:parentMethod()  -- "Child method (overridden)"
-- child:childMethod()

📋 게임 엔진용 실전 템플릿

1. 청크 클래스 예제

-- ============================================
-- Chunk: 맵 생성용 청크
-- ============================================
 
Chunk = {}
Chunk.__index = Chunk
 
function Chunk.new(patternType, difficulty)
    local self = setmetatable({}, Chunk)
    
    self.patternType = patternType or "default"
    self.difficulty = difficulty or 1
    self.blocks = {}
    self.length = 0
    
    return self
end
 
function Chunk:generate()
    -- 청크 생성 로직
    print("Generating chunk: " .. self.patternType)
end
 
function Chunk:addBlock(block)
    table.insert(self.blocks, block)
    self.length = self.length + 1
end
 
function Chunk:getBlockCount()
    return #self.blocks
end
 
-- 사용:
-- local chunk = Chunk.new("jump", 2)
-- chunk:generate()
-- chunk:addBlock({x=0, y=0})

2. 적(Enemy) 클래스 예제

-- ============================================
-- Enemy: 적 캐릭터
-- ============================================
 
Enemy = {}
Enemy.__index = Enemy
 
function Enemy.new(x, y, maxHp)
    local self = setmetatable({}, Enemy)
    
    self.x = x or 0
    self.y = y or 0
    self.maxHp = maxHp or 100
    self.hp = self.maxHp
    self.isAlive = true
    
    return self
end
 
function Enemy:takeDamage(damage)
    self.hp = self.hp - damage
    if self.hp <= 0 then
        self.hp = 0
        self.isAlive = false
        self:onDeath()
    end
end
 
function Enemy:heal(amount)
    self.hp = math.min(self.hp + amount, self.maxHp)
end
 
function Enemy:onDeath()
    print("Enemy died at " .. self.x .. ", " .. self.y)
end
 
function Enemy:update(deltaTime)
    if self.isAlive then
        -- 업데이트 로직
    end
end
 
-- 사용:
-- local enemy = Enemy.new(100, 200, 150)
-- enemy:takeDamage(50)
-- enemy:update(0.016)

3. Boss 클래스 (상속 예제)

-- ============================================
-- Boss: Enemy를 상속받는 보스
-- ============================================
 
Boss = setmetatable({}, {__index = Enemy})
Boss.__index = Boss
 
function Boss.new(x, y, maxHp, phase)
    -- 부모 생성자 호출
    local self = setmetatable(Enemy.new(x, y, maxHp), Boss)
    
    -- Boss 전용 속성
    self.currentPhase = phase or 1
    self.maxPhase = 3
    self.specialAttackCooldown = 0
    
    return self
end
 
-- 오버라이드: 체력이 특정 비율 이하면 페이즈 전환
function Boss:takeDamage(damage)
    -- 부모 메서드 호출 (없으면 직접 구현)
    Enemy.takeDamage(self, damage)
    
    -- Boss 전용 로직
    local hpPercent = self.hp / self.maxHp
    if hpPercent <= 0.5 and self.currentPhase == 1 then
        self:changePhase(2)
    elseif hpPercent <= 0.2 and self.currentPhase == 2 then
        self:changePhase(3)
    end
end
 
function Boss:changePhase(newPhase)
    self.currentPhase = newPhase
    print("Boss entered phase " .. newPhase)
end
 
function Boss:specialAttack()
    if self.specialAttackCooldown <= 0 then
        print("Boss special attack!")
        self.specialAttackCooldown = 5.0
    end
end
 
function Boss:update(deltaTime)
    -- 부모 update 호출
    Enemy.update(self, deltaTime)
    
    -- Boss 전용 업데이트
    self.specialAttackCooldown = math.max(0, self.specialAttackCooldown - deltaTime)
end
 
-- 사용:
-- local boss = Boss.new(500, 300, 1000, 1)
-- boss:takeDamage(600)  -- 페이즈 2로 전환
-- boss:specialAttack()

📋 빠른 참조 치트시트

-- ============================================
-- 치트시트: 빈 클래스 뼈대
-- ============================================
 
ClassName = {}
ClassName.__index = ClassName
 
function ClassName.new(param)
    local self = setmetatable({}, ClassName)
    self.param = param
    return self
end
 
function ClassName:method()
    -- 구현
end
 
-- ============================================
-- 치트시트: 상속 클래스 뼈대
-- ============================================
 
ChildClass = setmetatable({}, {__index = ParentClass})
ChildClass.__index = ChildClass
 
function ChildClass.new(param1, param2)
    local self = setmetatable(ParentClass.new(param1), ChildClass)
    self.param2 = param2
    return self
end
 
-- ============================================
-- 주의사항
-- ============================================
-- 1. new()는 점(.) 사용
-- 2. 메서드는 콜론(:) 사용
-- 3. self는 자동으로 첫 번째 인자
-- 4. 부모 메서드 호출: ParentClass.method(self, args)

💡 자주 사용하는 패턴들

-- 싱글톤 패턴
GameManager = {}
GameManager.__index = GameManager
local instance = nil
 
function GameManager.getInstance()
    if not instance then
        instance = setmetatable({}, GameManager)
        instance:init()
    end
    return instance
end
 
function GameManager:init()
    self.score = 0
end
 
-- 정적 메서드 (클래스 메서드)
function GameManager.staticMethod()
    -- self 없이 사용
    print("This is a static method")
end
 
 
-- 프라이빗 변수 (클로저 사용)
function createCounter()
    local privateCount = 0  -- 외부에서 접근 불가
    
    local Counter = {}
    
    function Counter.increment()
        privateCount = privateCount + 1
    end
    
    function Counter.getCount()
        return privateCount
    end
    
    return Counter
end

이 템플릿들을 복사해서 프로젝트에 넣어두고 필요할 때마다 참고하세요! 어떤 부분이 더 필요하신가요?