[2026] C++ Lua 스크립팅 완벽 가이드 | Lua C API·스택·테이블·바인딩 [실전]

[2026] C++ Lua 스크립팅 완벽 가이드 | Lua C API·스택·테이블·바인딩 [실전]

이 글의 핵심

게임 로직·플러그인·핫 리로드가 필요할 때 C++에 Lua를 붙이는 방법. Lua C API, lua_State, 스택 연산, C++↔Lua 데이터 전달, 테이블 조작, 흔한 에러, 베스트 프랙티스, 프로덕션 패턴까지 실전 코드로 정리합니다.

들어가며: “스킬 밸런스 하나 바꾸려고 전체 빌드 15분 걸려요”

실제 겪는 문제 시나리오

게임·도구·플러그인 시스템을 만들 때 자주 겪는 상황입니다. 문제: 게임 로직·밸런스·이벤트 처리를 C++에 직접 넣으면, 수정할 때마다 전체 재컴파일이 필요합니다. 해결: Lua 같은 경량 스크립트 언어를 C++ 엔진에 붙여서, 런타임에 스크립트만 바꿔도 로직을 갱신할 수 있게 합니다. 비유하면 “집(C++ 엔진)은 그대로 두고, 인테리어(로직)만 바꿀 수 있는 것”과 같습니다. 아래 코드는 mermaid를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 실행 예제
flowchart TD
  subgraph wrong[❌ C++ 하드코딩]
    W1[밸런스 수정] --> W2[소스 수정]
    W2 --> W3[전체 재빌드 15분]
    W3 --> W4[테스트]
    W4 --> W5[반복 비용 큼]
  end
  subgraph right[✅ Lua 스크립팅]
    R1[밸런스 수정] --> R2[Lua 파일만 수정]
    R2 --> R3[재시작 또는 핫 리로드]
    R3 --> R4[즉시 테스트]
    R4 --> R5[빠른 반복]
  end

이 글에서 다루는 것:

  • 문제 시나리오: Lua 스크립팅이 필요한 실제 상황
  • Lua C API: lua_State, 스택 연산, 데이터 타입
  • 완전한 예제: C++→Lua, Lua→C++, 테이블 조작
  • 자주 발생하는 에러와 해결법
  • 베스트 프랙티스프로덕션 패턴 요구 환경: C++17 이상, Lua 5.3 이상 (권장: Lua 5.4)

실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.

목차

  1. 문제 시나리오: Lua 스크립팅이 필요한 순간
  2. Lua C API 기본
  3. 스택 연산 상세
  4. C++에서 Lua로 데이터 전달
  5. Lua에서 C++로 데이터 전달
  6. 테이블 조작
  7. 완전한 Lua 스크립팅 예제
  8. 자주 발생하는 에러와 해결법
  9. 베스트 프랙티스
  10. 프로덕션 패턴
  11. 구현 체크리스트

1. 문제 시나리오: Lua 스크립팅이 필요한 순간

시나리오 1: 게임 밸런스 수정 시마다 15분 빌드

문제: 스킬 데미지, 이동 속도, 아이템 드롭률 같은 밸런스 값이 C++ 상수로 박혀 있습니다. 기획자가 “이 스킬 데미지를 100에서 120으로” 요청할 때마다 C++ 수정 → 전체 빌드 → 테스트가 반복됩니다. 해결: Lua 테이블로 밸런스 데이터를 분리하고, 런타임에 로드합니다. 스크립트만 수정하면 재시작 없이 적용 가능합니다. 아래 코드는 lua를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

-- balance.lua
return {
    skill_damage = 120,
    move_speed = 5.0,
    drop_rate = 0.15
}

시나리오 2: 사용자 플러그인·모드 지원

문제: 에디터·도구에서 사용자가 커스텀 동작을 추가하고 싶어 합니다. C++ 플러그인 DLL은 빌드 환경이 복잡하고, 보안 위험도 있습니다. 해결: Lua 스크립트로 플러그인 API를 노출하면, 사용자가 스크립트만 작성해 확장할 수 있습니다. 샌드박스로 제한된 API만 제공해 안전하게 합니다.

시나리오 3: 이벤트·퀘스트 시퀀스

문제: 퀘스트·이벤트·대화 시퀀스가 복잡한 조건 분기로 이어집니다. C++에 하드코딩하면 가독성과 유지보수가 어렵습니다. 해결: Lua 테이블이나 스크립트로 이벤트 시퀀스를 정의하면, 기획·스크립터가 직접 수정하기 쉽습니다. 아래 코드는 lua를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다, 비동기 처리를 통해 효율적으로 작업을 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

-- quest_events.lua
function on_quest_start(quest_id)
    if quest_id == 1 then
        spawn_npc("merchant", 100, 200)
        show_dialog("Welcome, adventurer!")
    end
end

시나리오 4: AI·행동 트리

문제: NPC 행동 로직이 C++에 있으면, “공격 거리 5 → 7로 바꿔볼까?” 같은 작은 실험을 10번 하려면 30분이 걸립니다. 해결: Lua로 행동 트리·AI 조건을 작성하면, 스크립트만 수정해 빠르게 반복할 수 있습니다.

시나리오 5: 설정 파일·데이터 테이블

문제: JSON·XML 파싱은 오버헤드가 있고, C++에서 직접 수정하기 어렵습니다. 해결: Lua 테이블은 문법이 간단하고, dofile로 로드하면 바로 Lua 값으로 사용할 수 있습니다.

2. Lua C API 기본

lua_State란?

lua_State*는 Lua 가상 머신의 핸들입니다. 모든 Lua C API 함수는 이 포인터를 첫 인자로 받습니다. Lua와 C++ 간의 모든 데이터 교환스택을 통해 이루어집니다. 아래 코드는 mermaid를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

flowchart TB
    subgraph cpp[C++]
        A[게임 엔진] --> B[lua_State*]
    end
    subgraph lua[Lua]
        B --> C[스택]
        C --> D[값/함수/테이블]
        B --> E[글로벌 환경]
    end

Lua 초기화 및 종료

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

extern "C" {
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
}
#include <string>
#include <stdexcept>
class LuaEngine {
    lua_State* L_ = nullptr;
public:
    LuaEngine() {
        L_ = luaL_newstate();
        if (!L_) {
            throw std::runtime_error("luaL_newstate failed");
        }
        luaL_openlibs(L_);  // base, table, string, math 등 표준 라이브러리
    }
    ~LuaEngine() {
        if (L_) {
            lua_close(L_);
            L_ = nullptr;
        }
    }
    lua_State* getState() const { return L_; }
};

주의점:

  • luaL_newstate(): 새 Lua VM 생성
  • luaL_openlibs(L): 표준 라이브러리(base, table, string, math, io, os 등) 로드
  • lua_close(L): VM 해제, 이후 L 사용 금지

스택 인덱스 규칙

Lua 스택은 1-based입니다. 스택 바닥은 1, 꼭대기는 lua_gettop(L)로 얻습니다.

인덱스의미
1스택 바닥 (가장 먼저 푸시된 값)
-1스택 꼭대기 (가장 최근 푸시된 값)
-2꼭대기에서 두 번째
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 스택 크기 확인
int top = lua_gettop(L);
// 인덱스 변환: 절대 인덱스 ↔ 상대 인덱스
// 양수: 바닥부터 1, 2, 3, ...
// 음수: 꼭대기부터 -1, -2, -3, ...

3. 스택 연산 상세

푸시 연산 (C++ → 스택)

다음은 cpp를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 정수
lua_pushinteger(L, 42);
// 부동소수
lua_pushnumber(L, 3.14);
// 문자열 (Lua가 내부 복사본 보관)
lua_pushstring(L, "hello");
// 불리언
lua_pushboolean(L, 1);   // true
lua_pushboolean(L, 0);   // false
// nil
lua_pushnil(L);
// C 함수를 Lua에 등록
lua_pushcfunction(L, my_c_function);
// light userdata (Lua가 GC하지 않음, 포인터만 저장)
lua_pushlightuserdata(L, ptr);
// nil 반환 (반환값 없을 때)
// return 0;

조회 연산 (스택 → C++)

다음은 cpp를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 타입 확인
int type = lua_type(L, index);
// LUA_TNIL, LUA_TBOOLEAN, LUA_TLIGHTUSERDATA, LUA_TNUMBER,
// LUA_TSTRING, LUA_TTABLE, LUA_TFUNCTION, LUA_TUSERDATA, LUA_TTHREAD
// 값 읽기 (타입 확인 후)
lua_Integer ival = lua_tointeger(L, index);
lua_Number   nval = lua_tonumber(L, index);
const char*  sval = lua_tostring(L, index);   // Lua가 소유, 수정 금지
bool         bval = lua_toboolean(L, index);
void*        pval = lua_touserdata(L, index);
// 안전한 조회 (타입 불일치 시 에러)
lua_Integer ival = luaL_checkinteger(L, 1);   // 인자 1이 정수가 아니면 에러
lua_Number  nval = luaL_checknumber(L, 2);
const char* sval = luaL_checkstring(L, 3);
// 선택적 조회 (기본값 사용)
lua_Integer ival = luaL_optinteger(L, 1, 0);  // 없으면 0
const char* sval = luaL_optstring(L, 2, "");

스택 조작

아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 꼭대기 값 제거 (1개)
lua_pop(L, 1);
// 인덱스 값을 꼭대기로 복사
lua_pushvalue(L, index);
// 스택 크기 설정 (늘리거나 줄임)
lua_settop(L, new_top);
// 인덱스 삽입 (해당 위치에 꼭대기 값 이동)
lua_insert(L, index);
// 스택 n개 제거
lua_pop(L, n);  // lua_settop(L, -(n)-1)와 동일

4. C++에서 Lua로 데이터 전달

C++ 함수를 Lua에 등록

다음은 cpp를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// C 함수 시그니처: int lua_cfunc(lua_State* L)
// 반환값: 스택에 남길 값의 개수
static int lua_add(lua_State* L) {
    lua_Integer a = luaL_checkinteger(L, 1);
    lua_Integer b = luaL_checkinteger(L, 2);
    lua_pushinteger(L, a + b);
    return 1;  // 반환값 1개
}
static int lua_log(lua_State* L) {
    const char* msg = luaL_checkstring(L, 1);
    printf("[Lua] %s\n", msg);
    return 0;  // 반환값 없음
}
void register_api(lua_State* L) {
    lua_register(L, "add", lua_add);
    lua_register(L, "log", lua_log);
    // lua_register는 lua_pushcfunction + lua_setglobal과 동일
}

upvalue로 C++ 객체 전달

Lua C 함수는 upvalue로 외부 데이터를 받을 수 있습니다. lua_pushcclosure로 클로저를 만들 때 upvalue를 묶습니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

struct GameEngine;
static int lua_create_entity(lua_State* L) {
    // upvalue 1에서 GameEngine* 가져옴
    auto* engine = static_cast<GameEngine*>(lua_touserdata(L, lua_upvalueindex(1)));
    if (!engine) return 0;
    int entity_id = engine->createEntity();
    lua_pushinteger(L, entity_id);
    return 1;
}
void register_entity_api(lua_State* L, GameEngine* engine) {
    lua_pushlightuserdata(L, engine);      // upvalue로 전달
    lua_pushcclosure(L, lua_create_entity, 1);  // upvalue 1개
    lua_setglobal(L, "create_entity");
}

C++ 구조체/객체를 Lua 테이블로 전달

아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

struct Vec2 {
    float x, y;
};
static int push_vec2(lua_State* L, const Vec2& v) {
    lua_createtable(L, 0, 2);
    lua_pushnumber(L, v.x);
    lua_setfield(L, -2, "x");
    lua_pushnumber(L, v.y);
    lua_setfield(L, -2, "y");
    return 1;  // 테이블 1개 푸시
}

5. Lua에서 C++로 데이터 전달

Lua 함수 호출 (C++에서)

bool call_lua_function(lua_State* L, const char* func_name, int a, int b) {
    lua_getglobal(L, func_name);
    if (!lua_isfunction(L, -1)) {
        lua_pop(L, 1);
        return false;
    }
    lua_pushinteger(L, a);
    lua_pushinteger(L, b);
    // lua_pcall(L, 인자 개수, 반환값 개수, 에러 핸들러 인덱스)
    if (lua_pcall(L, 2, 1, 0) != LUA_OK) {
        fprintf(stderr, "Lua error: %s\n", lua_tostring(L, -1));
        lua_pop(L, 1);
        return false;
    }
    lua_Integer result = lua_tointeger(L, -1);
    lua_pop(L, 1);
    printf("Result: %lld\n", (long long)result);
    return true;
}

Lua 테이블에서 C++로 값 읽기

다음은 cpp를 활용한 상세한 구현 코드입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// Lua: config = { damage = 100, speed = 5.0 }
void read_config(lua_State* L) {
    lua_getglobal(L, "config");
    if (!lua_istable(L, -1)) {
        lua_pop(L, 1);
        return;
    }
    lua_getfield(L, -1, "damage");
    int damage = lua_tointeger(L, -1);
    lua_pop(L, 1);
    lua_getfield(L, -1, "speed");
    double speed = lua_tonumber(L, -1);
    lua_pop(L, 1);
    lua_pop(L, 1);  // config 테이블 제거
    printf("damage=%d, speed=%.1f\n", damage, speed);
}

Lua 반환값 처리

아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// Lua: return a, b, c
// C++에서 여러 반환값 받기
void handle_multiple_returns(lua_State* L) {
    int nresults = lua_gettop(L);  // 반환값 개수
    if (nresults >= 1) {
        lua_Integer a = lua_tointeger(L, 1);
        // ...
    }
    if (nresults >= 2) {
        lua_Number b = lua_tonumber(L, 2);
        // ...
    }
    lua_settop(L, 0);  // 스택 비우기
}

6. 테이블 조작

C++에서 Lua 테이블 생성

다음은 cpp를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// Lua: t = { x = 10, y = 20, name = "player" }
void create_table(lua_State* L) {
    lua_createtable(L, 0, 3);  // 배열 부분 0, 해시 부분 3
    lua_pushinteger(L, 10);
    lua_setfield(L, -2, "x");
    lua_pushinteger(L, 20);
    lua_setfield(L, -2, "y");
    lua_pushstring(L, "player");
    lua_setfield(L, -2, "name");
    lua_setglobal(L, "t");
}

배열 형태 테이블

다음은 cpp를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// Lua: arr = { 10, 20, 30 }
void create_array(lua_State* L) {
    lua_createtable(L, 3, 0);  // 배열 3개
    lua_pushinteger(L, 10);
    lua_rawseti(L, -2, 1);
    lua_pushinteger(L, 20);
    lua_rawseti(L, -2, 2);
    lua_pushinteger(L, 30);
    lua_rawseti(L, -2, 3);
    lua_setglobal(L, "arr");
}

테이블 순회

다음은 cpp를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// Lua: for k, v in pairs(t) do ....end
// C++에서 테이블 순회
void iterate_table(lua_State* L, int table_index) {
    lua_pushnil(L);  // 첫 번째 키로 nil = 시작
    while (lua_next(L, table_index) != 0) {
        // 스택: ....key value
        // key는 -2, value는 -1
        if (lua_type(L, -2) == LUA_TSTRING) {
            const char* key = lua_tostring(L, -2);
            if (lua_isnumber(L, -1)) {
                lua_Number val = lua_tonumber(L, -1);
                printf("%s = %g\n", key, val);
            }
        }
        lua_pop(L, 1);  // value 제거, key는 다음 next용으로 유지
    }
}

Lua 테이블에서 C++로 구조체 읽기

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

struct Balance {
    int damage;
    double speed;
    std::string name;
};
bool read_balance_from_lua(lua_State* L, const char* table_name, Balance& out) {
    lua_getglobal(L, table_name);
    if (!lua_istable(L, -1)) {
        lua_pop(L, 1);
        return false;
    }
    lua_getfield(L, -1, "damage");
    out.damage = static_cast<int>(luaL_optinteger(L, -1, 0));
    lua_pop(L, 1);
    lua_getfield(L, -1, "speed");
    out.speed = static_cast<double>(luaL_optnumber(L, -1, 1.0));
    lua_pop(L, 1);
    lua_getfield(L, -1, "name");
    out.name = luaL_optstring(L, -1, "");
    lua_pop(L, 1);
    lua_pop(L, 1);  // 테이블 제거
    return true;
}

require로 로드한 모듈에서 테이블 가져오기

아래 코드는 lua를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

-- balance.lua
return {
    damage = 100,
    speed = 5.0,
    name = "default"
}

다음은 cpp를 활용한 상세한 구현 코드입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// C++에서 balance.lua 로드 후 테이블 사용
bool load_balance(lua_State* L, const std::string& path) {
    if (luaL_dofile(L, path.c_str()) != LUA_OK) {
        fprintf(stderr, "%s\n", lua_tostring(L, -1));
        lua_pop(L, 1);
        return false;
    }
    // 스택 꼭대기에 return된 테이블이 있음
    if (!lua_istable(L, -1)) {
        lua_pop(L, 1);
        return false;
    }
    lua_setglobal(L, "balance");  // balance라는 이름으로 저장
    return true;
}

7. 완전한 Lua 스크립팅 예제

예제 1: 게임 엔진 API 전체

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// game_lua.cpp
extern "C" {
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
}
#include <string>
#include <unordered_map>
#include <memory>
#include <cstdio>
struct Entity {
    int id;
    float x, y;
    std::string tag;
};
class EntityManager {
    int next_id_ = 0;
    std::unordered_map<int, Entity> entities_;
public:
    Entity& create_entity() {
        int id = next_id_++;
        entities_[id] = Entity{id, 0, 0, ""};
        return entities_[id];
    }
    Entity* get_entity(int id) {
        auto it = entities_.find(id);
        return it != entities_.end() ? &it->second : nullptr;
    }
    void destroy_entity(int id) { entities_.erase(id); }
};
class GameScripting {
    lua_State* L_;
    std::unique_ptr<EntityManager> entities_;
    int score_ = 0;
    static int lua_create_entity(lua_State* L) {
        auto* self = static_cast<GameScripting*>(lua_touserdata(L, lua_upvalueindex(1)));
        auto& e = self->entities_->create_entity();
        lua_pushinteger(L, e.id);
        return 1;
    }
    static int lua_set_position(lua_State* L) {
        auto* self = static_cast<GameScripting*>(lua_touserdata(L, lua_upvalueindex(1)));
        int id = static_cast<int>(luaL_checkinteger(L, 1));
        float x = static_cast<float>(luaL_checknumber(L, 2));
        float y = static_cast<float>(luaL_checknumber(L, 3));
        auto* e = self->entities_->get_entity(id);
        if (e) {
            e->x = x;
            e->y = y;
        }
        return 0;
    }
    static int lua_add_score(lua_State* L) {
        auto* self = static_cast<GameScripting*>(lua_touserdata(L, lua_upvalueindex(1)));
        int delta = static_cast<int>(luaL_checkinteger(L, 1));
        self->score_ += delta;
        return 0;
    }
    static int lua_destroy_entity(lua_State* L) {
        auto* self = static_cast<GameScripting*>(lua_touserdata(L, lua_upvalueindex(1)));
        int id = static_cast<int>(luaL_checkinteger(L, 1));
        self->entities_->destroy_entity(id);
        return 0;
    }
public:
    GameScripting() : entities_(std::make_unique<EntityManager>()) {
        L_ = luaL_newstate();
        luaL_openlibs(L_);
        lua_pushlightuserdata(L_, this);
        lua_pushcclosure(L_, lua_create_entity, 1);
        lua_setglobal(L_, "create_entity");
        lua_pushlightuserdata(L_, this);
        lua_pushcclosure(L_, lua_set_position, 1);
        lua_setglobal(L_, "set_position");
        lua_pushlightuserdata(L_, this);
        lua_pushcclosure(L_, lua_add_score, 1);
        lua_setglobal(L_, "add_score");
        lua_pushlightuserdata(L_, this);
        lua_pushcclosure(L_, lua_destroy_entity, 1);
        lua_setglobal(L_, "destroy_entity");
    }
    ~GameScripting() { lua_close(L_); }
    bool run_file(const std::string& path) {
        if (luaL_dofile(L_, path.c_str()) != LUA_OK) {
            fprintf(stderr, "Lua error: %s\n", lua_tostring(L_, -1));
            lua_pop(L_, 1);
            return false;
        }
        return true;
    }
    void fire_collision(int a, int b) {
        lua_getglobal(L_, "on_collision");
        if (lua_isfunction(L_, -1)) {
            lua_pushinteger(L_, a);
            lua_pushinteger(L_, b);
            if (lua_pcall(L_, 2, 0, 0) != LUA_OK) {
                fprintf(stderr, "on_collision error: %s\n", lua_tostring(L_, -1));
                lua_pop(L_, 1);
            }
        } else {
            lua_pop(L_, 1);
        }
    }
    int get_score() const { return score_; }
};

Lua 게임 로직 스크립트

아래 코드는 lua를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

-- init.lua: 게임 초기화
local player = create_entity()
set_position(player, 100, 200)
local coin = create_entity()
set_position(coin, 150, 250)
-- collision_handler.lua: 충돌 시
function on_collision(a_id, b_id)
    add_score(10)
    destroy_entity(b_id)
end

예제 2: 밸런스 테이블 로드

아래 코드는 lua를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

-- balance.lua
return {
    skill_damage = 120,
    move_speed = 5.0,
    drop_rate = 0.15,
    levels = { 100, 250, 500, 1000 }
}

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// C++에서 밸런스 로드
struct GameBalance {
    int skill_damage;
    double move_speed;
    double drop_rate;
    std::vector<int> levels;
};
bool load_balance(lua_State* L, const std::string& path, GameBalance& out) {
    if (luaL_dofile(L, path.c_str()) != LUA_OK) {
        return false;
    }
    if (!lua_istable(L, -1)) {
        lua_pop(L, 1);
        return false;
    }
    lua_getfield(L, -1, "skill_damage");
    out.skill_damage = static_cast<int>(lua_tointeger(L, -1));
    lua_pop(L, 1);
    lua_getfield(L, -1, "move_speed");
    out.move_speed = lua_tonumber(L, -1);
    lua_pop(L, 1);
    lua_getfield(L, -1, "drop_rate");
    out.drop_rate = lua_tonumber(L, -1);
    lua_pop(L, 1);
    lua_getfield(L, -1, "levels");
    if (lua_istable(L, -1)) {
        int len = static_cast<int>(lua_rawlen(L, -1));
        out.levels.reserve(len);
        for (int i = 1; i <= len; ++i) {
            lua_rawgeti(L, -1, i);
            out.levels.push_back(static_cast<int>(lua_tointeger(L, -1)));
            lua_pop(L, 1);
        }
    }
    lua_pop(L, 1);
    lua_pop(L, 1);  // balance 테이블
    return true;
}

예제 3: CMake 빌드

아래 코드는 cmake를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(LuaGame LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
find_package(PkgConfig REQUIRED)
pkg_check_modules(LUA REQUIRED lua5.4)
add_executable(game_app main.cpp game_lua.cpp)
target_include_directories(game_app PRIVATE ${LUA_INCLUDE_DIRS})
target_link_libraries(game_app PRIVATE ${LUA_LIBRARIES})

아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# Ubuntu/Debian
sudo apt install liblua5.4-dev
# vcpkg
vcpkg install lua

8. 자주 발생하는 에러와 해결법

에러 1: “attempt to call a nil value (global ‘create_entity’)”

원인: C++에서 create_entity를 Lua에 등록하기 전에 스크립트가 실행됐거나, 등록 시 전역 이름이 다릅니다. 해결법: 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ✅ API 등록 후 스크립트 실행
void GameScripting::init() {
    register_api();      // create_entity 등 등록
    run_file("init.lua"); // 그 다음 스크립트 실행
}
-- ❌ 잘못된 예: create_entity 호출 시점에 아직 등록 안 됨
local id = create_entity()  -- nil 호출 에러

에러 2: “bad argument #1 to ‘set_position’ (number expected, got nil)”

원인: Lua에서 set_position(entity_id, x, y) 호출 시 entity_id가 nil이거나 잘못된 타입입니다. 해결법: 아래 코드는 lua를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

-- ❌ 잘못된 예
set_position(nil, 100, 200)
-- ✅ 올바른 예
local id = create_entity()
set_position(id, 100, 200)

아래 코드는 cpp를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// C++에서 방어 코드
static int lua_set_position(lua_State* L) {
    if (lua_gettop(L) < 3) {
        return luaL_error(L, "set_position(entity_id, x, y) requires 3 arguments");
    }
    if (!lua_isnumber(L, 1)) {
        return luaL_error(L, "entity_id must be a number");
    }
    // ...
}

에러 3: 스택 오버플로우 / 불균형

원인: lua_push*lua_pop 개수가 맞지 않아 스택이 쌓이거나 부족합니다. 해결법: 아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ✅ 반환값 개수 정확히
lua_pushinteger(L, result);
return 1;  // 1개 반환
// ✅ 에러 시 스택 정리
if (lua_pcall(L_, 2, 0, 0) != LUA_OK) {
    fprintf(stderr, "%s\n", lua_tostring(L_, -1));
    lua_pop(L_, 1);  // 에러 메시지 제거
}
// ✅ 호출 전후 스택 높이 일치 확인
int top_before = lua_gettop(L);
// ....작업 ...
lua_settop(L, top_before);  // 복원

에러 4: Lua userdata가 가리키는 C++ 객체 수명

원인: Lua userdata가 가리키는 C++ 객체가 먼저 파괴되면, Lua에서 접근 시 크래시가 발생합니다. 해결법:

// shared_ptr을 userdata로 저장하고, __gc 메타메서드에서 정리
// 또는 Lua가 참조하는 동안 C++ 객체 수명을 연장 (예: 엔진이 소유)
// lightuserdata는 Lua가 GC하지 않으므로, C++ 측에서 수명 관리 필수

에러 5: “module ‘xxx’ not found”

원인: Lua에서 require "mymodule"을 썼는데, package.path에 해당 경로가 없습니다. 해결법:

-- Lua 스크립트 상단에서 경로 추가
package.path = package.path ...";./scripts/?.lua"

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// C++에서 package.path 설정
lua_getglobal(L_, "package");
lua_getfield(L_, -1, "path");
std::string path = lua_tostring(L_, -1);
path += ";./scripts/?.lua";
lua_pop(L_, 2);
lua_pushstring(L_, path.c_str());
lua_setfield(L_, -2, "path");
lua_pop(L_, 1);

에러 6: lua_tostring 반환값 수명

원인: lua_tostring(L, i) 반환값은 Lua가 관리합니다. lua_pop 후에는 무효화됩니다. 해결법: 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ❌ 잘못된 예
const char* s = lua_tostring(L, -1);
lua_pop(L, 1);
printf("%s\n", s);  // s는 이미 무효화됐을 수 있음
// ✅ 올바른 예: 즉시 복사
std::string str = lua_tostring(L, -1);
lua_pop(L, 1);
printf("%s\n", str.c_str());

에러 7: lua_next 사용 시 테이블 무결성

원인: lua_next 순회 중에 테이블을 수정하면 undefined behavior입니다. 해결법: 순회할 값들을 먼저 수집한 뒤 별도로 수정합니다.

9. 베스트 프랙티스

1. 스택 균형 유지

  • 모든 C 함수에서 lua_push*return n 개수가 일치해야 합니다.
  • 에러 시 lua_pop으로 스택 정리 후 return 0 또는 lua_error.

2. luaL_check* / luaL_opt* 사용

  • lua_tointeger 대신 luaL_checkinteger로 타입 검증.
  • 잘못된 인자 시 Lua가 에러 메시지와 함께 중단.

3. upvalue로 상태 전달

  • 전역 변수 대신 upvalue로 this 포인터 전달.
  • 스레드 안전성과 명확한 소유권.

4. 에러 핸들러 (traceback)

아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

static int traceback(lua_State* L) {
    lua_getglobal(L, "debug");
    lua_getfield(L, -1, "traceback");
    lua_pushvalue(L, 1);
    lua_pushinteger(L, 2);
    lua_call(L, 2, 1);
    return 1;
}
// 사용: lua_pushcfunction(L, traceback); lua_insert(L, err_idx);
// lua_pcall(L, nargs, nresults, err_idx);

5. 스크립트 사전 컴파일

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// 반복 실행 시 load 한 번, pcall 여러 번
luaL_loadfile(L_, "update.lua");
// 매 프레임
lua_pushvalue(L_, -1);
lua_pcall(L_, 0, 0, 0);

6. local 사용 권장

아래 코드는 lua를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

-- ❌ 전역 변수 (느림)
player_id = create_entity()
-- ✅ 로컬 변수
local player_id = create_entity()

10. 프로덕션 패턴

패턴 1: 샌드박싱

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// 위험한 함수는 등록하지 않음
// luaopen_io, luaopen_os 등 제외
lua_State* L = luaL_newstate();
luaopen_base(L);
luaopen_table(L);
luaopen_string(L);
luaopen_math(L);
// luaopen_io(L);  // 제외
// luaopen_os(L);  // 제외

패턴 2: 스크립트 타임아웃

아래 코드는 cpp를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

static void lua_hook(lua_State* L, lua_Debug* ar) {
    (void)ar;
    static int count = 0;
    if (++count > 1000000) {
        luaL_error(L, "script timeout (instruction limit)");
    }
}
lua_sethook(L_, lua_hook, LUA_MASKCOUNT, 10000);
lua_pcall(L_, 0, 0, 0);
lua_sethook(L_, nullptr, 0, 0);

패턴 3: 핫 리로드

아래 코드는 cpp를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

void GameScripting::reload_script(const std::string& path) {
    if (luaL_dofile(L_, path.c_str()) != LUA_OK) {
        log_error("Reload failed: %s", lua_tostring(L_, -1));
        lua_pop(L_, 1);
        return;
    }
}

패턴 4: 스크립트 버전 검사

-- script_version: 2

아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

int get_script_version(const std::string& path) {
    std::ifstream f(path);
    std::string line;
    if (std::getline(f, line)) {
        // "script_version: N" 파싱
    }
    return 0;
}

패턴 5: LuaJIT 고려

  • LuaJIT 사용 시 JIT 컴파일로 10~50배 가속.
  • luajit 개발 패키지로 교체 가능.

11. 구현 체크리스트

  • luaL_newstate / lua_close 쌍 호출
  • API 등록 후 스크립트 실행
  • luaL_check* / luaL_opt*로 인자 검증
  • C 함수 반환값 개수 정확히
  • 에러 시 lua_pop으로 스택 정리
  • lua_tostring 반환값 즉시 복사
  • upvalue로 C++ 객체 전달
  • traceback 에러 핸들러 사용
  • 샌드박스: 위험 함수 제외
  • 스크립트 타임아웃 (후크)
  • package.path 설정 (require 사용 시)

정리

항목설명
lua_StateLua VM 핸들, 모든 API의 첫 인자
스택C++↔Lua 데이터 교환 통로, 1-based 인덱스
푸시lua_pushinteger, lua_pushstring, lua_pushcclosure
조회lua_tointeger, lua_tostring, luaL_checkinteger
테이블lua_createtable, lua_setfield, lua_getfield, lua_rawseti
upvaluelua_pushcclosure로 C++ 객체 전달
에러 처리lua_pcall + traceback, lua_pop 정리
핵심 원칙:
  1. 스택 균형을 항상 유지한다.
  2. luaL_check*로 인자 검증을 한다.
  3. upvalue로 상태를 전달한다.
  4. 샌드박스로 위험 함수를 제외한다.

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. 게임 로직, 밸런스 테이블, 플러그인, 핫 리로드, 사용자 스크립트 등에 활용합니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. Lua vs LuaJIT 차이는?

A. LuaJIT은 JIT 컴파일로 10~50배 빠르지만, Lua 5.2 수준 호환입니다. Lua 5.4 최신 기능이 필요하면 표준 Lua를, 성능이 중요하면 LuaJIT을 고려하세요.

Q. 더 깊이 공부하려면?

A. Lua 5.4 Reference ManualProgramming in Lua 책을 참고하세요.

참고 자료


한 줄 요약: Lua C API·스택·테이블·upvalue를 마스터하면 C++ 게임 엔진에 안정적인 Lua 스크립팅을 구축할 수 있습니다.

관련 글

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3