1. 소개


여러 페이즈로 나뉘는 게임들의 경우, 여러 버튼을 활용해서 각 페이즈의 세팅을 자동화하는 경우가 있습니다. 예를 들어, 제가 업로드한 하트 오브 크라운 한글 (스크립트)를 보면, 게임을 세팅하는 부분에서 레어 카드 버튼은 누를때마다 다른 일이 일어납니다. 한 번 누르면 레어 카드가 '제국 수도 카리쿠마'로, 다시 누르면 '요정여왕 엘룬'으로, 또다시 누르면 '제국 수도 카리쿠마'로 돌아갑니다. 이처럼 여러 함수를 번갈아가며 작동하게 하는 버튼을 만들어 보도록 합시다.

2. 테이블 준비

2-1. 구상

텍사스 홀덤은 손패 2장과 공용패 5장을 이용해 승부하는 포커 게임입니다. 이를 예시로 들어보겠습니다. 텍사스 홀덤의 진행은 다음과 같습니다:

  • 프리 플랍: 각 플레이어가 손패 2장을 받습니다.
  • 플랍 : 바닥에 공용 카드 3장을 오픈합니다.
  • 턴 : 바닥에 4번째 공용 카드를 오픈합니다.
  • 리버 : 바닥에 5번째 공용 카드를 오픈합니다.

프리 플랍 단계를 진행했다면 더 이상 프리 플랍을 진행하는 버튼은 필요치 않게 됩니다. 그러므로 버튼 한개를 이용해 위 네 가지 단계를 차례차례 진행하도록 해보겠습니다.

2-2. 테이블 준비

상단메뉴의 Objects > Tables 에서 Poker를 선택합니다. 그리고 버튼을 생성할 Red Checker 하나와, 포커를 진행할 Standard 카드 한 벌을 생성하여 테이블 하단부에 놓습니다. 그리고 왼쪽 메뉴의 Zone > Scripting을 통해 덱을 스크립트 구역으로 감싸줍니다.

스크립트 구역
카드게임을 진행하다 보면 덱이 바닥나는 경우가 있습니다. 이 경우, 원래 존재하던 덱의 GUID는 사라져서 존재하지 않는 오브젝트가 됩니다. 또한, 이 카드들을 다시 합쳐도 원래의 오브젝트가 아닌, 새로운 오브젝트로써 인식됩니다. 그러므로 덱의 GUID를 이용하여 스크립트에 정의하는 방법은 그닥 좋지 않은 방법이 될 수 있습니다. 그러므로 매 번 달라질 수 있는 덱의 GUID 대신 스크립트 구역을 이용하여 해당 구역의 덱을 찾아내는 방법을 사용합시다.
스크립트 구역 역시 하나의 오브젝트로 관리됩니다. 그러므로 GUID도 존재합니다. 스크립트 구역을 스크립트에 정의하기 위해서는 GUID를 이용합니다. 스크립트 구역을 마우스 오른쪽 클릭하면 자동으로 해당 스크립트 구역의 GUID가 복사됩니다. 실수로 마우스 왼쪽 클릭을 하는 경우 스크립트 구역이 지워지는데, 다시 생성할 경우 GUID가 방금 전과 달라질 수 있음에 주의하시기 바랍니다.

여기까지 완료했다면 테이블을 저장합니다.

3. 스크립트 작성


이번 스크립트는 특정 오브젝트에 작성하지 않고 Global.lua에 작성하도록 하겠습니다. 그러므로 self 예약어가 작동하지 않기에 각 오브젝트의 GUID를 미리 선언할 필요가 있습니다. Red Checker와 스크립트 구역의 GUID를 미리 정의합시다. 덱의 GUID는 필요치 않습니다. 이유는 상기했던 대로, 덱이 바닥나면 덱 오브젝트는 사라지고, 다시 덱을 구성해도 새로운 오브젝트로 인식되어 GUID가 달라지기 때문에, 미리 GUID를 선언해도 덱이 바뀌면 소용이 없어지기 때문입니다.

redchecker_guid = '8776ff'
scriptzone_guid = '747ddc'
3-1. 버튼 만들기

버튼의 Parameter를 작성하고, onload()함수를 통해 게임이 불려오면 버튼이 생성되도록 합니다. 위와 마찬가지로 Global.lua에서는 오브젝트를 먼저 정의한 뒤에, 버튼을 생성해야 합니다. 스크립트 구역도 함께 정의해줍니다.

button_parm = {
    click_function = 'phase_preflop',
    function_owner = Global,
    label = '프리-플롭',
    tooltip = '각 플레이어가 손패 2장을 받습니다',
    width = 1000,
    height = 400,
    position = {0, 0.5, 0},
    font_size = 200,
}

function onload()
    scriptzone = getObjectFromGUID(scriptzone_guid)
    redchecker = getObjectFromGUID(redchecker_guid)
    redchecker.createButton(button_parm)
end

Global.lua에 작성하는 스크립트이므로 function_owner가 Global인 점에 주의하세요.

3-2. 덱 정의하기

이제 스크립트 구역에서 덱을 찾아 정의하도록 하는 스크립트를 작성해 봅시다. Bag 안에서 물건을 찾는 스크립트와 방식은 동일합니다. 잠시 기억을 되돌려 Bag 안의 물건을 찾는 함수인 getObject()의 활용을 생각해봅시다.

getObjects() 함수
getObjects함수는 스크립트 구역과 Bag에 사용할 수 있으며, 이에 반환되는 정보가 조금 다릅니다. 우선 반환 결과물부터 설명하자면

elements = Bag.getObjects()

위처럼 getObjects()함수를 Bag 혹은 덱에 사용할 경우, elements에 담기는 결과물은 아래와 같습니다:

{ {첫 번째 요소의 정보들}, {두 번째 요소의 정보들}, ... {마지막 요소의 정보들} }

예를 들면 책 한 권을 받는 것과 비슷합니다. 여러 페이지로 되어있고, 각 페이지 안에는 해당 오브젝트의 정보(이름, 순서, guid, 등)이 담겨있는 셈입니다. 그러나 스크립트 구역에 getObjects()를 사용하면 다른 결과를 얻습니다.

elements = Scriptzone.getObjects()

스크립트 구역에 getObjects() 함수를 사용하면 elements에 담기는 결과물은 아래와 같습니다:

{ 첫 번째 오브젝트, 두 번째 오브젝트, ... 마지막 오브젝트 }

이는 오브젝트를 줄세우는 것과 비슷합니다. elements 테이블의 각 요소에 오브젝트를 정의해서 넣어주는 셈입니다. 같은 getObjects()함수인데 이런 차이가 발생하는 이유는 Bag 혹은 덱에 들어있는 오브젝트는 게임에 존재하지 않아 오브젝트를 정의해줄 수 없지만, 스크립트 구역의 오브젝트는 게임 내에 존재하여 오브젝트를 직접 정의해줄 수 있기 때문입니다. getObjects()함수를 사용할 때에는 이에 주의하여 사용해야 합니다.
주의해야 하는 상황을 조금 더 구체적으로 예를 들면, elements테이블에 있는 첫 번째 요소의 이름을 읽어낸다고 하는 경우, Bag에 getObjects()함수를 사용한 경우라면 elements[1]['name']이라고 입력해야 하지만, 스크립트 구역에 있는 첫 번째 요소의 이름을 읽어내는 경우에는 elements[1].getName()을 이용해야 합니다.

함수 작성
우선, 스크립트 구역에 getObjects()함수를 통해 스크립트 구역 내 오브젝트 목록을 작성합니다. 이후 for문과 if문을 조합하여 각 오브젝트의 tag를 비교하여 'Deck'이라면 return을 이용해 덱을 반환합니다.

function getDeck()
    scriptzone_objects = scriptzone.getObjects()

    for i=1, #scriptzone_objects do
        if scriptzone_objects[i].tag == 'Deck' then
            return scriptzone_objects[i]
        end
    end
end
3-3. 버튼 함수 작성

덱에서 카드를 나눠줄 때, 덱을 우클릭하여 deal 버튼을 누르면 모든 플레이어에게 손패를 한장씩 나눠주게 됩니다. 이와 같은 기능을 하는 함수 역시 deal(숫자)입니다. 입력한 숫자만큼 각 플레이어에게 패를 나눠줍니다. 앞서 작성한 getDeck()함수를 이용해 덱을 정의하고, deal()함수로 간단하게 phase_preflop()함수를 작성해 봅시다.

function phase_preflop()
    deck = getDeck()
    deck.deal(2)
end

플레이어가 프리-플롭 버튼을 클릭하면, 더 이상 프리-플롭 버튼은 필요하지 않습니다. 그러므로 프리-플롭 버튼을 수정하여 다음 단계인 '플롭' 버튼을 만들어 줄 필요가 있습니다. 버튼을 수정할 때 사용되는 함수는 editButton(Parameter)함수입니다.

editButton()
editButton(Parameter)함수는 이미 존재하는 버튼을 수정할 때 사용합니다. 필요한 Parameter역시 createButton과 한가지를 제외하곤 모두 동일합니다. 한 가지 다른 요소는 바로 index입니다. index는 버튼의 번호로, 해당 오브젝트에 버튼이 여럿 있다면 부여되는 번호입니다. index는 0에서 시작하며, 이번 예제에서는 Red Checker에 버튼이 단 하나이므로 index는 0입니다. 아래와 같이 Parameter를 작성하여 phase_perflop()함수에 추가합니다. editButton()의 Parameter에는 버튼을 생성할 때와는 달리 수정할 요소만 작성하면 나머지 요소는 기존 버튼의 요소를 유지합니다.

editbutton_parm = {
    index = 0,
    click_function = 'phase_flop',
    label = '플롭',
    tooltip = '바닥에 공용패 3장을 오픈합니다',
}
redchecker.editButton(editbutton_parm)

이제 phase_flop()함수를 작성합니다. takeObject(Parameter)함수를 이용하여 덱에서 카드를 뽑아 지정된 위치로 이동합니다. 뽑은 카드의 이동도 takeObject()함수의 Parameter를 활용하도록 합니다. takeObject()함수의 활용은 이전 강좌를 참고하시기 바랍니다.

일반항 찾기
3번을 반복해서 뽑는 동작이므로 for문을 활용하면 좋을 것 같습니다. 하지만 뽑는 카드의 좌표가 매 번 달라져야 합니다. 다만 일정한 간격으로 나열하기 때문에 우리는 일반항을 찾아 n번째의 좌표를 식으로 만들어줄 수 있습니다.
각 카드의 좌표는 다음과 같습니다.

{-5.60, 1.30, -1.97}, {-2.80, 1.30, -1.97}, {0, 1.30, -1.97}

각 좌표는 순서대로 {x, y, z} 값을 의미합니다. 값들을 보면 x좌표가 2.8씩 증가하고 나머지는 변하지 않는 등차수열입니다. 잠시 고등학교 수학시간으로 돌아가면 초기값은 -5.6, 공차는 +2.8입니다. 이를 식으로 작성하면:

x = -5.6 + (n-1)*2.8

function phase_flop()
    deck = getDeck()

    for n=1, 3 do
    deck.takeObject({
        position = {-5.60 + (n-1)*2.8, 1.30, -1.97},
        flip = true
    })
    end
end

이어서 버튼을 수정하는 함수를 작성해줍니다.

editbutton_parm = {
    index = 0,
    click_function = 'phase_turn',
    label = '턴',
    tooltip = '4번째 공용패를 오픈합니다',
}
redchecker.editButton(editbutton_parm)

이후, 동일한 방법으로 phase_turn()함수와 phase_river()함수를 작성하고, 리버 단계가 끝나면 다시 버튼을 프리-플롭으로 되돌립니다.

function phase_turn()
    deck = getDeck()

    deck.takeObject({
        position = {2.80, 1.30, -1.97},
        flip = true
    })

    editbutton_parm = {
        index = 0,
        click_function = 'phase_river',
        label = '리버',
        tooltip = '마지막 공용패를 오픈합니다',
    }
    redchecker.editButton(editbutton_parm)
end

function phase_river()
    deck = getDeck()

    deck.takeObject({
        position = {5.60, 1.30, -1.97},
        flip = true
    })

    editbutton_parm = {
        index = 0,
        click_function = 'phase_preflop',
        label = '프리-플롭',
        tooltip = '각 플레이어가 손패 2장을 받습니다',
    }
    redchecker.editButton(editbutton_parm)
end

여기까지 했다면 완성입니다.

4. 스크립트 전문


redchecker_guid = '8776ff'
scriptzone_guid = '747ddc'

button_parm = {
    click_function = 'phase_preflop',
    function_owner = Global,
    label = '프리-플롭',
    tooltip = '각 플레이어가 손패 2장을 받습니다',
    width = 1000,
    height = 400,
    position = {0, 0.5, 0},
    font_size = 200,
}

function onload()
    scriptzone = getObjectFromGUID(scriptzone_guid)
    redchecker = getObjectFromGUID(redchecker_guid)
    redchecker.createButton(button_parm)
end

function getDeck()
    scriptzone_objects = scriptzone.getObjects()

    for i=1, #scriptzone_objects do
        if scriptzone_objects[i].tag == 'Deck' then
            return scriptzone_objects[i]
        end
    end
end

function phase_preflop()
    deck = getDeck()
    deck.deal(2)

    editbutton_parm = {
        index = 0,
        click_function = 'phase_flop',
        label = '플롭',
        tooltip = '바닥에 공용패 3장을 오픈합니다',
    }
    redchecker.editButton(editbutton_parm)
end

function phase_flop()
    deck = getDeck()

    for n=1, 3 do
        deck.takeObject({
            position = {-5.60 + (n-1)*2.8, 1.30, -1.97},
            flip = true
        })
    end

    editbutton_parm = {
        index = 0,
        click_function = 'phase_turn',
        label = '턴',
        tooltip = '4번째 공용패를 오픈합니다',
    }
    redchecker.editButton(editbutton_parm)
end

function phase_turn()
    deck = getDeck()

    deck.takeObject({
        position = {2.80, 1.30, -1.97},
        flip = true
    })

    editbutton_parm = {
        index = 0,
        click_function = 'phase_river',
        label = '리버',
        tooltip = '마지막 공용패를 오픈합니다',
    }
    redchecker.editButton(editbutton_parm)
end

function phase_river()
    deck = getDeck()

    deck.takeObject({
        position = {5.60, 1.30, -1.97},
        flip = true
    })

    editbutton_parm = {
        index = 0,
        click_function = 'phase_preflop',
        label = '프리-플롭',
        tooltip = '각 플레이어가 손패 2장을 받습니다',
    }
    redchecker.editButton(editbutton_parm)
end

5. 마무리


오늘은 editButton()함수를 활용해봤습니다. editButton()함수를 잘 활용하면 단계별 진행이 필요한 게임에서 각 단계에 필요한 버튼만 배열하여, 초심자에게 더 친근한 스크립트가 될 수 있습니다. 궁금하신 점 댓글로 달아주시면 아는 범위에서 최대한 답변드리도록 하겠습니다. 읽어주셔서 감사합니다.

  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기