루아에서 테이블은 마치 배열처럼 사용할 수 있는데 인덱스가 1로 부터 시작한다는 것이 특이하다.


          tA = {10,11, 20, 30, 40}

          print(tA[1]) -- 10 이 찍힌다

          print(tA[3]) -- 20 이 찍힌다


하지만 인덱스를 0으로부터 시작시킬 수도 있다.


          tB = {[0]=10,11, 20, 30, 40}

          print(tB[1]) -- 11 이 찍힌다

          print(tB[3]) -- 30 이 찍힌다


테이블이름 앞에 #을 붙이면 배열의 크기를 구할 수 있는데, 엄밀히 얘기하면 맨 마지막 자연수키를 반환하는 것 같다.


          print(#tA) -- 5가 찍힌다

          print(#tB) -- 4가 찍힌다


만약 tA의 한 요소를 삭제하기 위해서 nil로 지정하면, 예를 들어서


          tA[4]=nil

          print(tA[4]) -- nil 이 찍힌다

          print(#tA) -- 여전히 5가 찍힌다.


이때 오해하기 쉬운게 4번째 요소가 nil로 사라졌으니 tA의 크기는 4로 줄어야 되는 것 아니냐 하는 것인데 여전히 5이다. 4번째 요소는 nil로 바뀌었을뿐 여전히 자리를 차지하고 있다.

  완전히 삭제하려면 table.remove()를 써야한다.


          table.remove(tA, 4)

          print(tA[4]) -- 40이 찍힌다.

          print(#tA) -- 이제 4가 찍힌다.


즉, table.remove()함수를 사용하면 그 즉시로 배열의 인덱스값이 달라진다. 원래 인덱스가 5였던 것이 4로 바뀌는 것이다. 이 사실은 반복문 안에서 table.remove()함수를 사용할 때 반드시 고려해야 한다.


  프로그램을 작성하다보면 필요에 의해서 객체를 동적으로 생성한 후 (몬스터, 총알 등등) 배열에 집어넣게 된다. 그리고 어떤 조건에 맞으면 (화면에서 벗어났다던가) 그것을 삭제해야 되는데 그 조건 검사를 보통 for문으로 다음과 같이 하게 된다.


          for id=1, #tA do

                    ...

                    if condition1 == true then

                              ...

                              tA[id]:revmoveSelf()

                              tA[id] = nil

                    end

                    ...

          end


그냥 이렇게 하는 걸로는 충분하지 않은 이유는 배열의 크기는 그대로이기 때문에 새로운 객체가 생성될 때마다 배열이 계속 커지게 된다. 시간이 지날수록 조건 검사의 부담이 늘어날 것이다. 그래서 table.remove()를 다음과 같이 써야 한다.


          for id=1, #tA do

                    ...

                    if condition1 == true then

                              ...

                              tA[id]:removeSelf()

                              table.remove(tA, id)

                    end

                    ...

          end


그런데 이렇게 하면 모든 요소에 대해서 제대로 검사가 수행이 되지 않는데 그 이유는 table.remove()함수가 실행되면 그 즉시로 인덱스가 변하기 때문에 하나를 건너뛰게 되기 때문이다. 예를 들어 4번 요소가 조건이 맞아서 삭제되면 원래 5번이었던 것이 4번이 되고 그 다음 반복에서는 5번이(원래는 6번 이었던 것) 검사가 되기 때문이다.


  간단한 해법은 역순으로 검사를 하는 것이다.


          for id=#tA, 1, -1 do

                    ...

                    if condition1 == true then

                              ...

                              tA[id]:removeSelf() 

                              table.remove(tA, id)

                    end

                    ...

          end


이렇게 하면 table.remove()가 실행되어도 이후에 검색할 요소의 인덱스는 변하지 않으므로 모든 배열 요소에 대해서 조건검사가 수행이 되게 된다.


Posted by 살레시오
,

  잘 알려져 있다시피 클래스(class)는 객체지향 프로그램에서 핵심적인 역할을 하고 있고 루아에서는 이 기능을 베이스 수준에서 지원하지는 않지만 어느 정도 흉내는 낼 수 있다. 사실 객체지향에서 중요한 특성으로 캡슐화, 상속, 다형성 세 가지 정도가 언급이 되지만 소규모 프로젝트에서는 캡슐화 정도만 어느 정도 구현되어도 코딩과 수정 그리고 디버깅이 상당히 용이해진다고 개인적으로 생각한다. 글을 쓰고 있는 본인도 깊이 있는 지식은 없으므로 여기에서는 초보자들이 간단하게 쓸 수 있는 정도로만 설명하고자 한다.


  

  객체지향이나 클래스의 개념이 생소하다면 일단은 ‘특정한 임무에 관련된 변수들과 그 변수들을 핸들링하는 관련 함수의 집합’ 정도로 이해해도 될 것 같다. 예를 들어서 ‘좌표점과 그것에 관련된 계산’이라는 임무에 대해서


   (1) x좌표

   (2) y좌표

   (3) 한 좌표의 원점으로부터의 거리를 구하는 함수

   (4) 두 점의 거리를 구하는 함수


정도를 구현한다고 하자. 보통 (1),(2)번을 멤버변수라고 하고 (3)(4)번은 멤버함수라고 한다. 이것들을 전체를 하나의 이름으로 묶은 것을 클래스라고 한다.


코로나에서 이것을 외부 모듈로 구현한다면 먼저 다음과 같은 형태를 생각해 볼 수 있다. (외부모듈에 대한 기본적인 것은 이전 포스트를 참조)


┌─────────────────────────────


     local Sqrt = math.sqrt

     local M={}

     

     function M.New(x,y)

          local pt = {x=x or 0, y = y or 0} -- 먼저 멤버변수를 테이블로 새로 생성


          function pt:GetLength() -- 첫 번째 멤버함수를 pt안에서 생성

               return Sqrt(self.x*self.x + self.y*self.y)

          end


          function pt:DistTo(pt2) -- 두 번째 멤버함수를 pt 안에서 생성

               local dx = self.x - pt2.x

               local dy = self.y - pt2.y

               return Sqrt(dx*dx + dy*dy)

          end


          return pt -- 생성된 테이블(인스턴스)를 반환한다.


     end


     return M


└─────────────────────────────


  이 예제에서는 M.New() 함수 안에서 새로운 테이블을 생성한 후 이 안에서 변수와 함수를 다 정의하여 반환하는 식으로 처리했다. 이것을 예를 들어서 ‘point.lua’라고 저장했다면 다른 파일(예를 들어서 main.lua)에서 다음과 같이 불러서 쓸 수 있다.


┌── "main.lua" ───────────────────────────


     local CPoint = require "point" -- 외부모듈을 읽어들인다.


     local pt1 = CPoint.New(10,20) -- 첫 번째 인스턴스 생성

     local pt2 = CPoint.New(30,40) -- 두 번째 인스턴스 생성


     print("length of pt1:".. pt1:GetLength() ) -- 길이 22.36이 찍힘

     print("length of pt2:".. pt2:GetLength() ) -- 길이 50이 찍힘


     print("distance:".. pt1:DistTo(pt2) ) -- 두 점의 거리 28.28이 찍힌다


└─────────────────────────────


이제 CPoint.New()함수를 호출해서 새로운 점좌표를 얼마든지 생성할 수 있으며 보통 이렇게 생성되는 객체를 인스턴스(instance)라고 부른다. 그리고 이렇게 생성된 인스턴스를 통해서 관련 함수를 호출할 수 있다. (print 함수 안의 명령들)


  그런데 이 point1.lua 의 단점은 인스턴스를 생성할 때 마다 그 인스턴스 안에 함수의 본체도 같이 구현된다는 것이다. 예를 들어 100개를 생성하면 함수 본체도 각각 100개가 존재한다. 멤버함수의 개수나 덩치가 커진다면 이것은 실행이나 메모리 관점에서 굉장히 비효율적이다. 그래서 다음과 같이 멤버함수는 외부로 빼는 방식을 생각해 볼 수 있다.


┌─────────────────────────────

          local Sqrt = math.sqrt

          

          local function GetLength(tbl) -- 함수 본체를 외부에 정의

                    return Sqrt(tbl.x*tbl.x + tbl.y*tbl.y)

          end


          local function DistTo(pt1, pt2) -- 함수 본체를 외부에 정의

                    local dx = pt1.x - pt2.x

                    local dy = pt1.y - pt2.y

                    return Sqrt(dx*dx + dy*dy)

          end


          local M={}


          function M.New(x,y)

                    local pt = {x=x or 0, y = y or 0}


                    function pt:GetLength() -- 본체로 리다이렉션시킨다

                              return GetLength(self)

                    end


                    function pt:DistTo(pt2) -- 본체로 리다이렉션시킨다

                              return DistTo(self, pt2)

                    end

                    

                    return pt

          end

          return M

└─────────────────────────────


이제 함수 본체는 (인스턴스 개수와 상관없이) 외부에 하나만 존재하며 인스턴스 안에는 단지 본체로 리다이렉션 시켜주는 조그만 함수가 있을 뿐이다. 앞의 경우보다는 훨씬 효율적이지만 여전히 (작은 크기지만) 함수가 인스턴스 내부에 존재하고 본체로 재호출한다는 점에서 비효율적이다.


  좀 더 루아스럽고 우아하게(...) 개선하려면 이전 포스트에서 설명한 메타테이블의 __index 를 사용하면 된다.


┌─────────────────────────────

          local Sqrt = math.sqrt

          

          local mtIndex = {}


          function mtIndex:GetLength()

                    return Sqrt(self.x*self.x + self.y*self.y)

          end


          function mtIndex:DistTo(pt2)

                    local dx = self.x - pt2.x

                    local dy = self.y - pt2.y

                    return Sqrt(dx*dx + dy*dy)

          end


          local M={}


          function M.New(x,y)

                    local pt = {x=x or 0, y=y or 0} -- 멤버변수를 생성

                   return setmetatable(pt, {__index = mtIndex}) -- 멤버 함수를 메타테이블로 첨부한 후 반환

          end

          

          return M

└─────────────────────────────


이 방법이 코딩의 간결성이나 실행의 효율성에서 앞에서 소개한 방법들 보다 좀 더 앞선다고 할 수 있다.


Posted by 살레시오
,

  루아에서는 테이블에 메타테이블(metatable)이라는 것을 붙일 수 있다. 이 메타테이블에 미리 정해진 필드가 채워져 있다면 이것에 의해서 메타테이블이 붙어 있는 원래 테이블의 동작(특성)을 바꿀 수 있다.

  어떤 테이블에 메타테이블을 붙이는 것은 setmetatable()이라는 함수를 사용한다.



          setmetatable(tA, mtA) -- 테이블 tA에 메타테이블 mtA를 첨가

          tB = setmetatable({}, mtA) -- 빈 테이블에 메타테이블을 mtA를 첨가한 것을 tB에 반환


메타테이블에는 정해진 문자열 키값을 갖는 테이블을 필드로 가져야 되는데 이 미리 정해진 문자열 키값들은 __index, __newindex, __call, __tostring, __add 등등이 있다.

  이 중에서 __index 에 대해서만 간단히 설명하면 다음과 같다. 만약 테이블 tA 의 키값으로 요소들을 접근한다고 할 때(tA.nA, tA[1], tA[“FuncA”] 등등) 그 키값이 tA에 없을 때에는 nil 을 반환할 것이다. 하지만 tA에 메타테이블이 연결되어 있다면 그 연결된 메타테이블의 __index 테이블에 등록된 필드를 추가로 검사한다. 예를 들어서



          local tA={x=10}

          print(tA.y) -- "y"라는 키값이 없으므로 nil 이 찍힌다.

          local mt = { __index = { y = 20 } }

          setmetatable(tA, mt)
          print( tA.y ) -- 메타테이블에 있는 20이 찍힌다.



메타테이블의 __index 내부에는 함수도 물론 정의할 수 있다.



          local mtIndex = {}

          function mtIndex:sum()
                    return self.x + self.y — self는 메타테이블이 붙은 원래 테이블

          end

          local tA={x=10, y=20}
          setmetatable( tA, { __index=mtIndex } )

          print( tA:sum() ) -- 30이 찍힌다


          local tB = setmetatable( {x=30, y=40}, {__index = mtIndex} )
          print( tB:sum() ) -- 70이 찍힌다



위의 예제는 간단하지만 이것을 이해했다면 루아(코로나)에서 객체지향을 간단하게나마 구현하는데 응용할 수 있다.


Posted by 살레시오
,