본문 바로가기

프로그래밍/Love2D

Love2D(LÖVE)로 플랫포머 게임 개발해보자! (1) 기초 문법 정리

원래라면 지난주에 글을 썼어야 했지만.... 지스타 참여 이슈로 이번에 글을 쓰게 됐다.

원래라면 바로 그래픽 연동부터 들어가려 했지만.... 생각보다 이해가 안돼서 기본부터 차근차근 쌓아가기로 결정했다.

원래라면 로그를 봐가며 개발을 진행해야 했지만.... VSCODE로는 print 등의 출력문을 보는 방법을 모르겠다. 이건 나중에 방법 발견하면 관련해서 내용 추가하도록 하겠다.

잡설은 이쯤하고, 슬슬 정리를 시작해 보겠다.

 


콜백 함수

유니티도 고도도 으레 그렇듯, love2d에도 콜백 함수는 존재한다. 여기서 콜백 함수란, 유저가 따로 구현하지 않아도 해당 프레임워크에서 알아서 수행되는 함수를 뜻한다. 예를 들어 update문은 프레임마다 지속적으로 호출해 주고, start는 처음 프로그램이 구동될 때 한 번만 실행되는 함수가 콜백 함수인 것이다.

 

그럼, love2d에서는 어떤 콜백 함수를 사용할까? 일단 가장 많이 쓰일만한 것만 정리해 보았다.

function love.load()
	-- 코드 실행시 최초 한번만 실행되는 함수.
    -- 유니티의 Awake와 같은 역할.
end

love.load()는 코드가 실행될 때 최초 한 번만 실행되는 함수다. 게임이 구동될 때 가장 먼저 실행되며, 유니티의 Awake와 비슷한 느낌이라 생각하면 될 것 같다.

최초 실행 함수면 아무래도 변수들을 초기화하거나 필요한 리소스를 불러오는 역할을 수행하는 편이 적합할 것이다. 최초 한번만 초기화를 진행해 주면 이후 함수에서 지속적인 초기화 없이 변수를 사용할 수 있을 것이기 때문.

 

function love.update(dt)
	-- 게임이 실행될때 지속적으로 실행되는 함수
    -- 유니티의 update 느낌
    -- 여기서 dt는 deltatime
end

love.update()은 지속적으로 실행되는 함수다. 게임의 시작부터 끝까지 쭉 실행이 되며, 유니티의 Update와 비슷한 느낌이라 생각하면 될 것 같다.

그럼 이쯤에서 궁금증이 생긴다. love.load()와 love.update 중 어느 게 더 먼저 실행될까? 이거에 대해선 원래라면 print를 이용해 직접 실험해 볼 수 있었지만, 지금은 여건이 안 되는 관계로 비슷한 질문글을 찾아봤다.

 

What is the point of love.load? - LÖVE

Questions about the LÖVE API, installing LÖVE and other support related questions go here. Forum rules Before you make a thread asking for help, read this. 128Gigabytes Prole Posts: 14 Joined: Wed Jan 04, 2017 5:53 pm Post by 128Gigabytes » Wed Jan 04,

love2d.org

대충 요약하면 아래와 같다.

- love.boot()가 먼저 실행되고, 이때 love 모듈이 로드된다.

- love.init()가 실행되고, 이때 main.lua를 탐색하여 해당 코드를 실행한다.

- love.run()가 실행되고, 이때 콜백 함수들이 실행된다. 이때 love.load가 최초로 실행되고, 이후 love.update나 love.draw(후술 예정) 등 루프 함수가 실행되기 시작한다.

 

결국 love.load가 최우선으로 실행되고 love.update가 그제야 실행되기 시작하니, 이점 감안하고 코드를 짜내려가면 될 것 같다.

 

function love.draw()
    --그래픽 처리는 무조건 이곳에서 처리됨
    --다른 곳에서는 그래픽관련 함수는 전혀 안먹힘
    --그래서 변수를 다른 곳에서 처리해서 그래픽을 움직이게 하는 방법도 있음 
end

love.draw()는 게임 내에서 그래픽처리 전반을 담당하는 콜백 함수다. love2d가 따로 GUI툴을 갖추지 않은 프레임워크이기 때문에 이렇게 그래픽 처리를 수행하는 함수를 따로 빼둔 듯싶다. love.draw 또한 update처럼 매 프레임마다 실행되기 때문에, 이곳에서 사용된 변수가 외부에서 변하면 이곳에도 그대로 적용된다.

이외에도 love.mousepressed, love.quit, love.keypressed 등등 다양한 함수가 있는데, 이것들은 필요할 때 검색해서 확인해도 괜찮으니 넘어가도록 하겠다.

 

 

변수 선언

그럼 함수는 됐다. 변수는 어떻게 선언하는가? 사실 변수 선언방식이 기존에 쓰던 방식과 달라서 많이 헷갈렸다. 일단 아래 예문 코드를 보자.

function love.load()
    val="hello world!"
end

function love.draw()
    love.graphics.print(val, 100, 100)
end

우선, love2d는 자료형의 선언을 요구하지 않는다. 파이썬처럼 내가 적당히 변수에 아무 값이나 넣어두면, 그걸 프레임워크가 알아서 인식해 준다. 그래서 이런 식으로 초기화를 하고, 출력은 love.draw에서 수행하면 된다. 위의 코드를 실행하면 아래와 같은 결과가 나올 것이다.

 

물론 전역변수로도 사용할 수 있다. 굳이 love.load에서 선언하지 않고, 함수 바깥에서 변수를 선언해도 코드는 잘 실행된다. 

temp="hello world!! 2"

function love.draw()
    love.graphics.print(temp, 100, 100)
end

 

 

전역변수와 지역변수의 개념도 활용해 볼 수 있다. local 키워드를 변수명 앞에 붙여주면 된다. 아래 코드로 한번 확인해 보자.

local temp="hello outer world!!!"
function love.load()
    local temp="hello inner world!!!"
end

function love.draw()
    love.graphics.print(temp, 100, 100)
end

코드를 보면, love.load함수 밖에 local로 선언된 변수가 있고, love.load내부에 선언된 local변수가 있다. 이 경우, love.load 내부의 temp는 실행되지 않고, love.load 바깥에 있는 변수가 실행된다. 실행 결과는 아래와 같다.

여기서 local은 굳이 유니티로 비교하자면 private라고 생각하면 될 것 같다. 그런데 어차피 love.load에서 선언한 변수도 다른 함수에서 사용한 걸 보면, local은 함수 내에서만 사용되어야 하는 변수 같은 게 아닌 이상 그다지 활용할 일이 많지는 않을 것 같다고 조심스럽게 생각해 본다. 물론 이것도 외부 함수 참조하기 시작하고 나서부턴 중요도가 꽤 높아지겠지만 지금은 크게 중요한 건 아니니...

 

당연하게도 합차연산도 가능하다. 사칙연산 뭐 그런 거 말이다. 그러나 lua는 ++, --나 +=1 같은 연산은 먹히지 않으니 이점 유의하도록.

 

함수

함수는 진짜 별거 없다. 리턴이 있는 경우와 없는 경우를 하나의 예제에 담아보겠다.

function love.load()
    temp=0
    temp=plusNum(temp)
end

function sayNum(num)
    love.graphics.print(num, 100, 100)
end

function plusNum(num)
    return num+1
end

function love.draw()
    sayNum(temp)
end

love.load에서 0 값을 가지는 변수를 선언, plusNum에서 1을 더한 값을 반환받아 값이 변경됐다. 이 결과는 love.draw에서 sayNum 함수를 호출하여 그래픽적으로 출력하게 된다. 그리하여 출력 결과는 아래와 같게 된다.

 

조건문과 반복문

조건문과 반복문도 뭐.... 특별할 게 없다. 조건문부터 살펴보도록 하자.

function love.load()
    temp=0
end

function love.update()
    if love.keyboard.isDown("down") and temp>0 then
        temp=temp-1
    elseif love.keyboard.isDown("up") or love.keyboard.isDown("right") then
        temp=temp+1
    end
end

function love.draw()
    love.graphics.print(temp, 100, 100)
end

보시다시피 love2d는 if, elseif, else의 형태로 조건문을 사용한다. 다중조건을 걸고 싶으면 and나 or을 사용하면 된다. 위 코드를 실행하면 위 키나 오른쪽 키를 누르면 수치가 증가, 아래키를 누르되 변수가 0 초과일 때만 수치가 감소함을 확인할 수 있을 것이다.

 

다음은 반복문이다. 반복문은 크게 while문과 for문으로 나뉜다. 각자 장단점이 있긴 하지만, 여기서는 for문을 중점적으로 살펴볼 계획이다. 참고로 while문은 아래와 같이 사용한다.

while condition do
	--코드
end

 

for문은 어떻게 보면 파이썬의 그것과 비슷하다고 할 수는 있다. 아래 코드를 보면 그 유사성을 더더욱 느낄 수 있을 것이다.

function love.load()
    temp1=100
    temp2=100

    for i=1, 10 do
        temp1=100+10*i
    end

    for i=1, 10, 2 do
        temp2=100+10*i
    end
end

function love.draw()
    love.graphics.print(temp1, 100, temp1)
    love.graphics.print(temp2, 200, temp2)
end

위의 코드를 실행하면 아래와 같은 결과가 나온다.

for문은 처음 초기화할 변수, 목푯값, 값이 오르는 주기 순으로 조건이 입력된다. 여기서 값이 오르는 주기를 생략하면 자동적으로 1씩 증가하게 된다. 참고로 위의 값이 연속적으로 각 숫자가 나온 게 아니라 반복의 마지막 값만 결과로 나오게 됐는데, 이건 우리가 지속적으로 "새로" print 한 게 아니라 "기존의 값"을 갱신했기 때문이다. 그럼 각 반복의 과정이 하나하나 보이게 하려면 어떻게 해야 하는가?

 

이 해답은 리스트에 있다.

 

리스트

리스트는 자료구조의 일종으로, 여러 개의 값들이 마치 하나의 목록처럼 나열되어 있는 자료형을 뜻한다. 바로 선언부터 알아보자. 위의 코드를 살짝 변형해서 선언해 보도록 하겠다.

function love.load()
    temp1={}
    temp2={}

    for i=1, 10 do
        temp1[i]=100+10*i
    end

    for i=1, 10, 2 do
        temp2[i]=100+10*i
    end
end

function love.draw()
    for i=1, 10 do
        love.graphics.print(temp1[i], 100, temp1[i])
    end

    for i=1, 10, 2 do
        love.graphics.print(temp2[i], 200, temp2[i])
    end
end

위 코드의 실행 결과는 아래와 같다.

아, 이제 좀 반복의 과정이 눈에 훤하다. 리스트는 {}의 형식으로 선언되며, 이 리스트의 각 인덱스에 값을 넣을 수 있다. 여기서 temp2를 좀 눈여겨봐야 하는데, temp2의 리스트는 값이 띄엄띄엄 저장됐다. 그 이유는 애초에 반복문이 값을 1이 아닌 2씩 증가시켰기 때문에 인덱스도 2칸씩 건너뛰게 됐기 때문이다.

"여기서 더 파고들면 이해하기 어려울 것 같다...!!"

여기서 우리는 한 가지 해결책을 사용할 수 있다. 다른 프로그래밍 언어들이 으레 그렇듯, 배열의 "길이"를 가져올 수 있지 않던가? love2d는 이 리스트의 길이를 가져오는 방식이 매력적이다. 아래 코드를 보자. 참고로 방금 코드의 변형이다.

function love.load()
    temp1={}
    temp2={}

    for i=1, 10 do
        temp1[i]=100+10*i
    end

    for i=1, 10, 2 do
        temp2[i]=100+10*i
    end
end

function love.draw()
    for i=1, #temp1 do
        love.graphics.print(temp1[i], 100, temp1[i])
    end

    for i=1, #temp2 do
        love.graphics.print(temp2[i], 200, temp2[i])
    end
end

실행 결과는 아래와 같다.

어..... temp2의 길이가 1로 처리되나 보다. 아무래도 짝수 인덱스에 있는 값은 아예 공백도 아닌 없는 값 처리되는 듯하다. 이 부분에 대해 자세히 알고 있으면 댓글을 달아주면 정리글 개선에 큰 도움이 될 것이다.

아무튼, love2d에서는 리스트의 길이값을 가져오려면 리스트명 앞에 #만 붙이면 된다. 그래서 간단하게 리스트의 길이를 알 수 있는 건 좋은 것 같다. 이것 외에도, foreach문 느낌의 문법도 지원하는데, 아래 코드를 한번 보자.

function love.load()
    temp1={}

    for i=1, 10 do
        temp1[i]=100+10*i
    end
end

function love.draw()
    for i, v in ipairs(temp1) do
        love.graphics.print(v, 100, temp1[i])
    end
end

실행 결과는 아래와 같다.

이번에는 ipairs를 사용해 봤다. "for i, v in ipairs(temp1) do"라는 문장에서 i는 현재 순회 중인 값의 인덱스, v는 현재 위치의 값을 표현한다. 그래서 "love.graphics.print(v, 100, temp1[i])" 처럼 v와 i를 혼합해서 사용해도 별 문제는 없다는 것이다.

 


지금껏 배운 내용들 정도면 아마 간단한 게임을 만드는 데에는 큰 문제는 없을 것이다. 사실상 게임 개발에 있어 기초적인 부분은 전부 다뤘기 때문이다. 오늘 정리한 내용들을 응용하면 아래와 같은 코드도 짜볼 수 있다.

-- 최초 실행.
function love.load()
    --현재 생성된 직사각형 목록을 저장할 리스트
    listOfRectangles={}
end

-- 지속 실행
function love.update(dt)
	--deltatime에 맞게 생성된 직사각형을 일정 속도로 이동시킴
    for i, v in ipairs(listOfRectangles) do
        v.x=v.x+v.speed*dt
    end
end

--그래픽 처리
function love.draw()
	--현재 생성된 직사각형들의 그래픽 처리
    for i, v in ipairs(listOfRectangles) do
        love.graphics.rectangle("line", v.x, v.y, v.width, v.height)
    end
end

--직사각형 생성부
function createRect()
	--직사각형에 대한 정보 정의를 rect에 저장함.
    rect={}
    rect.width=100
    rect.height=130
    rect.x=120
    rect.y=120

    rect.speed=100
    
    --현재 생성된 직사각형 목록에 rect를 추가하여 새로운 직사각형 등록
    table.insert(listOfRectangles, rect)
end

--키 입력 감지시 작동
function love.keypressed(key)
	--입력된 키가 스페이스바면 직사각형 생성 함수 호출
    if key=="space" then
        createRect()
    end
end

위 코드는 스페이스바를 누르면 특정한 위치에 오른쪽으로 이동하는 직사각형을 생성하는 코드다. 코드를 잘 보면 콜백 함수부터 조건문, 반복문, 리스트, 변수, 함수 등등 오늘 정리한 기초적인 내용들을 전부 활용하고 있는 모습을 확인할 수 있다. 이 코드를 응용하면 간단한 비행 슈팅 게임도 만들어볼 수 있을 것이다. 자세한 코드 설명은 강좌 링크가 있으므로, 아래 글을 참고할 것.

 

How to LÖVE

Learn how to program games with the LÖVE framework

sheepolution.com

 

이렇듯 love2d로 게임을 만든다는 건 기존의 게임개발과 완전히 다른 새로운 경험을 제공한다. 단순 시스템 알고리즘만 짜면 상관없던 시절에서 벗어나, 정말로 완전히 밑바닥부터 쌓아 올리는 느낌이라고 말할 수 있다. 그래픽 처리, 물리엔진 구현, 입력부 처리 등등 모든 걸 나 스스로 구현해내야 한다. 그것도 순수 코딩으로 말이다. 그래서 타 게임개발 엔진에 비해선 난도가 높은 편이지만, 그럼에도 완전히 내 힘으로 게임을 만든다는 게 매력적으로 다가온다고 생각한다. 그래서 최대한 제목처럼 간단한 게임 하나 완성시킬 수 있을 때까지 계속 공부해 나갈 계획이다.