2013년 9월 20일 금요일

루아(코로나)에서 변수의 참조에 대해서

  루아의 변수는 다음과 같이 8가지 데이터타입을 갖는다.

        nil
        부울값
        숫자
        문자열
        테이블
        함수
         ----------
         코루틴
         유저데이터

이 중에서 코루틴과 유저데이터는 코로나SDK와 별로 관련이 없으므로 제외하고 나머지 여섯 개 중 참조를 갖는 것은 함수와 테이블이다. 문자열은 참조가 아니라는 것에 유의해야 한다. 참조란 실제 데이터가 저장된 곳을 가리키는 주소이다. (사실 참조도 주소'값'이므로 루아는 다 값이라고 주장하기도 하지만 혼동의 여지가 있으므로 여기서는 참조와 값을 구분하도록 하겠다.)
   따라서 만약 어떤 테이블을 함수의 입력파라메터로 넘길 때 참조가 넘어가므로 그것을 받아서 조작하면 원래의 테이블값도 바뀌게 된다.



example 1

local function Func1(a, tA)
     a = 20
     tA.x = 20
end

local function Func2(tA)
     tA = {x=30}
end

local x, tX = 10 , {x=10}
Func1(x, tX) -- x는 값이, tX는 참조가 넘어간다.
print(x, tX.x) -- 10, 20 이 찍힌다.
Func2(tX)
print(tX.x) --20이 찍힌다.


  위의 예에서 Func1()을 호출할 때 로컬 변수 x는 값이 넘어가고 테이블 tX는 참조가 넘어가므로 함수 호출 후 테이블의 필드만 값이 변경되었다. 반면에 Func2()내에서는 tA가 tX의 참조를 받기는 하지만 새로운 테이블의 참조로 초기화되었다. 따라서 이경우 tA는 넘어온 tX와는 전혀 별개의 것이 되어서 tX에는 아무런 영향을 끼치지 못한다. 그래서 맨 마지막 print()에서 20이 찍히는 것이다.

  이와 마찬가지로 함수의 리턴값이 테이블이나 함수일 경우도 참조를 반환한다. 다음 예는 처음에는 조금 이해하기 힘들 수 있으나 한 번 살펴보도록 하겠다.



example 2

local function Func1(xR)
   local x = xR or 0 -- 내부변수 x를 넘어온 값으로 초기화
   local y = 0
   local Func = function() print("x :"..x);end -- 여기서 x가 사용되었다.
   return y, Func -- y는 '값'을, Func는 '참조'를 반환
end

local y1, Func2 = Func1(10)
local y2, Func3 = Func1(20)
Func3() -- 20 이 찍힌다.
Func2() -- 10 이 찍힌다.


함수 Func1()은 그 안에서 내부변수 y의 '값'과 내부변수 Func의 '참조'를 반환한다. 기본적으로 어떤 함수의 지역변수는 함수가 종료되면 사라진다는 것은 알고 있을 것이다. 변수 y는 함수가 종료되면 더 이상 참조되는 곳이 없으므로 GC의 타겟이 되어 사라질 것이다. 하지만 내부함수 Func는 그 참조가 호출된 곳의 Func2 변수를 통해서 계속 사용되므로 사라지지 않고 살아남게 된다. 그럼 내부변수 x는 어떨까? x변수는 Func안의 print()문에서 참조를 하고 있으므로 Func가 살아있는 한 x도 계속 살아있게 된다.
  또 한가지 주의할 것은 Func2 와 Func3에서 참조하는 변수 x는 서로 별개의 것이라는 점이다. 따라서 처음 Func3()호출에는 20이 찍히고 맨 마지막 Func2() 함수를 호출하면 10이 찍힌다. 서로 '다른' 변수 x를 참조하고 있기 때문이다.

  이것을 이해했다면 클래스를 구현할 때 외부에서는 접근할 수 없고 내부에서만 사용할 수 있는 private 변수/함수 를 구현하는데 응용할 수 있다.

2013년 9월 19일 목요일

Maxima 개요 (2)

  이번 학기(2012년 1학기)에 <공학수학1> 교과목에 적용해 보려고 Maxima를 날마다 조금씩 공부하면서 강의용 부교재도 작성하고 있다. 당연한 이야기지만 프로그램 하나를 익히는 데에도 인내심을 가지고 시간을 꾸준히 투자해야 한다는 것을 새삼 느끼고 있다.


Prof. William F. Schelter (1947-2001. 수학자, Maxima를 유지, 발전시킴)

  이런 무료 프로그램이 한두 개가 아니긴 하지만 긴 세월 동안 이 프로그램을 개발해오고 지금도 시간을 들여서 업그레이드를 해 주고 있는 익명의 프로그래머들이 참 대단하게 느껴진다. 뭔가 다른 수익 모델도 없어 보이는데 말이다.









  사용에 큰 불만은 없는데 하나 꼽아보라면 전반적으로 명령어 기반의 프로그램이라서 사용하기 조금 불편하다는 것이다. 위의 예를 보면 알겠지만 연습 문제 풀이용 그림을 하나 그리려고 해도 몇 줄의 명령이 들어가야 한다. 그리고 자연 로그 함수의 이름이 log() 여서 수학책의 log함수(밑수가 10인)와 혼동된다는 점 정도이다. 자연 로그는 ln(), 그냥 로그 함수는 log()였으면 좋았을 텐데.

  이 내용을 학생들이 잘 따라와 줄까 하는 점도 살짝 걱정이 되긴 하는데 딱딱한 수식만 나열하면서 수업을 하는 것 보다는 흥미를 끌 수는 있을 것 같다. 시간이 되면 기본 사용법 동영상도 몇 개 만들어서 공개를 할 것이다.

Maxiam 개요

  학부생들에게는 (공대생이라 할 지라도) 수학 계산에 쓰이는 프로그램이 낯설 것이다. 여기서 '수학 계산에 쓰이는 프로그램'이라는 것은 윈도우를 깔면 기본으로 제공되는 계산기 프로그램을 말하는 것이 아니라 수학 문제를 해결하는데 사용되는 전문적인 프로그램을 말하는 것이다.

  사실 공대에 들어와서 <일반 수학>이나 <공학 수학>같은 과목들을 배울 때에는 손으로 풀거나 공학용 계산기 같은 것으로 함수 입력해서 그래프를 그리고 하는 방식이 아직까지는 익숙한 풍경이다. 학부생들이 전문적인 컴퓨터 프로그램을 이용한다는 것은 그리 일반적이지는 않은 것 같다. '곱셈이나 나눗셈' 하면 '계산기'가 보통은 떠오르지만 '방정식'을 푸는데 컴퓨터로 그 식들을 입력하고 엔터키를 치면 그 해를 간단히 구할 수 있다는 것을 아는 학부생들이 얼마나 될까.

  이러한 수학 과목에서 매스메티카(mathematica)나 매트랩(matlab)을 실습에 도입한 교재들이 간간히 있기는 하다. 하지만 이들 패키지는 고가의 상용 제품들이므로 학부생들이 자유롭게 실습에 이용하기에는 제약이 따른다. 하지만 훌륭한 대안이 있는데 바로 Maxima와 Sage라는 프리웨어들이다.


  이 그림의 첫번째 명령에서 보면 입력방정식이 으로 a와 b라는 기호를 어떤 수치값을 갖는 변수가 아니라 문자상수로 취급하여 그 해를 보여줌을 알 수 있다. 이렇게 문자 상수를 숫자처럼 다루는 것을 기호식 해석(symbolic analysis)라 하고 mathematica, maple, maxima, sage와 같은 프로그램이 사용된다. 반면, 변수 이외의 모든 기호는 어떤 수치값을 지정하여 문제를 푸는 것을 수치 해석(numerical analysis)라고 하고 일반 계산기 (프로그램), MATLAB, SciLab, Octave, FreeMat 등의 프로그램이 사용된다.



  나의 관심은 <공학 수학>의 교과 과정에 Maxima나 Sage 같은 프로그램의 사용법을 접목시킬 수 있는가이다. 공과 대학의 수학 과목들이 학생들이 주어진 공학적 문제를 수학적으로 모델링하고 그것을 해결하는 능력을 배양하는 데에 목적이 있지만 이제는 컴퓨터를 이용한 문제 해결 능력도 같은 비중으로 다루어져야 한다고 개인적으로 생각하기 때문이다. 즉, 기존에는 수학에서 컴퓨터 이용 능력이 보조적인 역할로 다루어졌지만 이제는 동등하거나 혹은 더 비중있게 다루어져야 하지 않을까 하는 생각이 든다.

2013년 9월 16일 월요일

About homing missile algorithm (Corona SDK)

  In my recently released game (Lunar Blast) that is developed using Corona SDK, a homing missile (that tracks moving enemy) have been adopted as one of the sub-weapons. In this blog post, I will explain the detailed tracking algorithm developed.


 Let's consider the following figure.


  The current x- and y-coordinates and rotational angle of the missile are marked as x_m[k], y_m[k], and theta_m[k], respectively. The index [k] designates current frame. Enemy target is randomly determined and the current x- and y-coordinates of the target are described as x_t[k] and y_t[k].

  In every enterFrame events, the following calculations are performed. Direction vector must be firstly obtained by subtracting missile x and y values from those of the target.

          dxt = x_t[k] - x_m[k]
          dyt = y_t[k] - y_m[k]

The rotating angle that can be obtained by this direction vector is a desired (perfect) one for the missile. However, setting this rotating angle directly to the missile make the curving movement very steep and awkward. For the smooth turning movement, the following algorithm has been developed.

The unit vector (length 1) of this vector is obtained by dividing these values by its length.

          dist = math.sqrt ( dxt*dxt + dyt*dyt )
          dxt = dxt/dist
          dyt = dyt/dist

The increment of x and y values of the missile can be calculated by the following equations. These equations play a key role to make the missile move smoothly.

          dx[k] = ga * dx[k-1] + (1-ga) * dxt
          dy[k] = ga * dy[k-1] + (1-ga) * dyt

In this equations, variable ga has a value of real number between 0 to 1. The closer to 0 ga is (for example, 0,2, 0.1, 0,05, etc), the faster the turning speed is. If the ga is close to 1, (for example 0,9, 0.95, 0,99 etc) the curving speed becomes very slow. The dx[k-1] ( or dy[k-1] ) means the dx (or dy) value of the previous frame. Thus, these values are to be stored for the next frame.

  Finally, the updated coordinates and rotational angle of the missile is calculated as follows

          x_m[k+1] = x_m[k] + dx[k]*vel
          y_m[k+1] = y_m[k] + dy[k]*vel
          theta_m[k+1] = atan2(dy[k], dx[k])*_r2d + 90

where variable vel is a linear speed of the missile and _r2d is a (constant) variable that converts radian to degree. (_r2d = 180/3.14). The vel value determines moving speed of the missile.

  The actual code sinppet of Update() function of the homing missile is here. Every enterFrame events call this Update() function. Variable bb is the missile sprite object.



You can check the movement of the homing missile with the algorithm in the following video clip.


The similar algorithm is also applied to the movement of side planes, and to the slow rotation of the big final bosses etc.

2013년 9월 15일 일요일

코로나로 구현한 타겟을 추종하는 알고리듬

  아래 그림 (... 대충 그렸습니다. )으로 설명하겠습니다.


현재 프레임에서의 미사일 좌표와 각도를 각각 x_m[k], y_m[k], theta_m[k] 라고 하겠습니다. [k]는 현재 프레임을 나타내는 시간 인덱스입니다. 이 미사일의 타겟은 현재 화면 상의 적들 중에서 임의로 하나 잡습니다. 미사일이 추종해야 할 적의 좌표는 x_t[k], y_t[k] 라고 쓰겠습니다. 이제 매 프레임마다 다음과 같은 연산을 수행합니다.
  적좌표에서 미사일의 좌표를 빼면 미사일이 향해야 할 방향벡터가 나옵니다

          dxt = x_t[k] - x_m[k]
          dyt = y_t[k] - y_m[k]

이것의 단위벡터(크기가 1인 벡터)를 계산합니다. 이것은 벡터의 크기로 각 요소를 나눠주면 됩니다. . 단위벡터를 계산하는 이유는 적과의 거리와 상관 없이 내 미사일은 동일한 속도로 움직여야하기 때문입니다.

          dist = math.sqrt ( dxt*dxt + dyt*dyt )
          dxt = dxt/dist
          dyt = dyt/dist

이제 미사일의 좌표의 실제 증분값은 다음과 같이 계산합니다. 다음의 방정식이 미사일이 부드럽게 움직이도록하는데 중요한 역할을 합니다.

          dx[k] = a * dx[k-1] + (1-a) * dxt
          dy[k] = a * dy[k-1] + (1-a) * dyt

여기서 상수 a 는 0과 1사이의 값이고 0에 가까울수록 추종하는 속도가 더 빨라지고 1에 가까운 값이면 천천히 추종하게 됩니다. 그리고 dx[k-1]과 dy[k-1]은 직전 프레임에서의 dx, dy값입니다. (이것을 차분방정식이라도 합니다. 미분방정식과 유사한 개념입니다.) 이 좌표로부터 미사일이 향할 각도도 계산됩니다.
  최종적으로 미사일의 좌표와 각도는 다음과 같이 매프레임마다 갱신됩니다.

          x_m[k] = x_m[k-1] + dx[k]*vel
          y_m[k] = y_m[k-1] + dy[k]*vel
          theta_m[k] = atan2(dy[k], dx[k])*_r2d + 90

여기서 vel은 미사일의 직선 속도값이고 _r2d 변수는 라디안을 도로 바꿔주는 수(_r2d = 180/3.141592)입니다. vel 값이 크면 미사일의 직선속도가 빠르게 되고 작으면 느리게 됩니다.

아래는 실제로 미사일의 update()함수에 사용된 코드의 일부분입니다. bb변수가 미사일 객체입니다.


  위에서 소개한 간단한 차분방정식은 여러 경우에 응용될 수 있습니다. 제 게임의 예를 들면 옵션기기가 본체와 약간 시차를 두고 따라온다던지 또는 최종보스가 나를 향해서 천천히 회전한다든지 하는데 사용되었습니다.


  여러분들도 한 번 적용해 보시기 바랍니다.

남의 것 탐하기

  컴퓨터를 사용하려면 필수적으로 OS를 비롯해서 그 OS상에서 실행되는 이런 저런 응용프로그램을 사용하게 되어 있다. 하지만 교육 기관이나 회사 컴퓨터가 아니고 일반 가정집이나 개인용으로 쓰이는 PC상에서 고가의 (수 만원짜리 저가라도 마찬가지일 것이다.) 프로그램을 직접 구입해서 사용하는 사람들의 비율이 얼마나 될까?

  비단 프로그램 뿐만 아니라 음악이나 영화 같은 저작권이 있는 저작물도 많은 이들이 큰 죄의식 없이 간단하게 다운로드 받아서 보고 듣고 즐기고 있으니 그런 것들을 정당한 비용을 치루고 사용해야 한다는 인식이 아직까지는 (특히 우리나라의 경우에는) 매우 약한 것 같다. 불법 다운로드는 주인이 있는 가게에 들어가서 물건을 훔치는 것과 별반 다를 바가 없다는 것을 알면서도 죄의식이 안드는건 소프트웨어가 무형의 자산이라는 것 때문일 것이다. 또한 인터넷 환경이 발달되고 P2P나 웹하드 같은 경로를 전문적인 지식이 없어도 너무나 쉽게 이용할 수 있다는 이유도 있을것이고.


  사실 나도 누구에게 돌을 던질 입장은 아니다. 불과 몇 년 전만 해도 아무런 죄의식 없이 이런 저런 것들을 너무나 당연한 듯이 다운받아서 사용했으니까. 하지만 어느 순간부터 (신앙심이 생기고 성당에서 봉사하기 시작한 이후에) 문제 의식을 느끼고 윈도우XP 시디부터 구매하게 되었고 이후에 내 컴퓨터에서 불법으로 사용하던 프로그램들을 삭제해 나가기 시작했다.

  자연스럽게 꼭 필요한 프로그램의 경우는 상용 프로그램을 대신할 수 있는 무료 (혹은 부분 무료) 프로그램들에 관심이 가기 시작했고 의외로 많은 유용한 프로그램들이 무료로 공개되고 또 사용되고 있다는 것을 알게 되었다. 그런 프로그램을 사용할 때 마다 별 다른 보상없이 묵묵히 시간을 투자해 주는 개발자들에게 경외심마저 든다. (그런 분들은 사실 남에게 도움이 되는 일을 한다는 자부심 자체가 보상이고 기쁨일 것이다. 그렇지 않고서는 계속 일을 해나갈 수 있을까?)
  이 블로그에 가끔씩 내가 사용해 왔던 프리웨어나 웹서핑하다가 발견한 유용한 무료 소프트웨어들을 소개하고 간단한 사용법을 설명하려고 한다.

corona sdk : xScale/yScale vs. obj:scale()

  The x/y scales of a display object (image, sprite, group... etc.) are stored in xScale/yScale properties. If a display object is created, the default values of these properties are 1s. There are two methods that modify scale of the display object. First one is direct setting xScale/yScale properties of the object and the other one is obj:scale(mx, my) function call. Actually, I had been misunderstanding that the two methods take the same action to the object. It can be easily misunderstood that obj:scale(mx, my) is the same as obj.xScale, obj.yScale = xm, ym ,

  However, the obj:scale(xm, ym) method performs multiplication xm/ym values and the xScale/yScale properties. That is, obj:scale(xm, ym) is the same as

   obj.xScale, obj.yScale = obj.xScale*xm, obj.yScale*ym

Let's see the following example.

   local img = display.newImage("some_image.png")
   img:scale(1.5, 2.0)
      .
      .
      .
   img:scale(0.5, 0.5)


The result of second img:scale() call is xScale = 0.75 (1.5 times 0.5) and yScale = 1. (2.0 times 0.5)

  The angle is stored in the obj.rotation property in degree.  The default value of this property is 0 when created. The modification of the angle of a display object is also performed by two methods. One is direct setting of obj.rotation property and the other method is obj:rotate(dr) function call. Similar to the former case, one must be cautious to the fact that the function obj:rotate(dr) adds dr degree (angle) to the obj.rotation property. That is obj:rotate(dr) is the same as obj.rotation = obj.rotation + dr.

2013년 9월 14일 토요일

루아(코로나)의 논리연산자 and, or, not

  루아는 C언어와 다르게 true, false 값만을 가지는 부울형이 있다.  C언어에서는 내부적으로 0값을 거짓(false)으로 취급하지만

          ‘루아는 nil 과 false 만을 거짓으로 간주한다’

는 점을 유의해야 한다. 정수 0도 루아에서는 진리값이 true이다. C언어에 익숙했다면 참 헷갈리기 쉬운 부분이다.

  이 사실을 염두에 두고 루아의 논리연산자를 살펴보자. 루아의 논리연산자는 and, or, not 세 가지가 있다. 각각의 동작을 정리하면 다음과 같다.

     ▪ not A - A가 거짓이면 (즉 nil 혹은 false이면) true, 아니면 false 반환
     ▪ A and B — A가 거짓이면 (즉 nil 혹은 false이면) A를 바로 반환
                      A가 참이면 B를 반환
     ▪ A or B — A가 참이면 (즉 nil도 false도 아니면) A를 바로 반환
                    A가 거짓이면 B를 반환

A and B 연산에서 A가 거짓이면 B는 아예 보지도 않고(계산도 안하고) 바로 A를 반환하며, 반대로 A or B 연산에서는 A가 참이면 B는 아예 보지도 않고 바로 A를 반환한다. 이것을 조금만 생각해 보면 not은 항상 부울값(true, false)을 반환하는데 반해서 and와 or는 그 반환값이 부울값이 아닐 수도 있다는 것을 알 수 있다. 몇 가지 예를 들어보면

          local a = 1 and 0 — a에는 0값이 이 저장됨
          local b = nil and 1 — b에는 nil 이 저장됨
          local c = 1 or 0 — c에는 1값이 이 저장됨
          local d = nil or 0 — d에는 0 이 저장됨

이것을 응용하면 예를 들어서 함수의 입력인자가 nil인지 아닌지를 따져서 내부변수를 다르게 초기화할 때 용이하다.

          local function Func( xR, yR )
                    local x, y
                    if xR==nil then
                              x = 0
                    else
                              x = xR
                    end
                    if yR==nil then
                              y = 0
                    else
                              y = yR
                    end

위와 같은 긴 코드를 다음과 같이 간단하게 한 줄로 줄일 수 있다.

          local function Func( xR, yR )
                    local x, y = xR or 0, yR or 0

또 다른 예로 어떤 테이블이 nil인지 아닌지를 따져서 그 내부 요소를 참조하고자 할 때도 유용하다. 예를 들면

          local x = tLoc.x

의 경우 만약 tLoc 테이블 자체가 nil 인 경우에는 변수 x에 nil 이 저장되는 것으로 오해하기 쉬운데 실제로는 런타임 에러가 발생하고 프로그램이 멈추게 된다. 개인적으로 코로나로 코딩하면서 초기에 많이 접한 것이 이런 종류의 런타임 에러이다. (반면 tLoc 테이블 자체는 nil이 아닌데 x요소가 없는 경우에는 x에 nil 이 저장되고 런타임 에러는 발생하지 않는다.)
  이러한 런타임 에러를 없애고 테이블 자체가 nil일 경우를 처리하려면 예를 들어서 다음과 같이 해야 할 것이다.

          local x
          if tLoc == nil then
                    x = nil
          else
                    x = tLoc.x
          end

위와 같은 코드를 and연산자를 이용하면 다음과 같이 한 줄로 바꿀 수 있다.
          local x = tLoc and tLoc.x

이와 같이 and 와 or 연산의 특성을 이용하면 프로그램을 간결하게 작성할 수 있다.

클래스 구현을 이용한 간단한 예제

내친김에 간단한 예제를 하나 만들어 보았습니다. (소스코드)



이 동영상 보시면 화면 위에서 운석이 생성되면 인스턴스가 하나 증가(table.insert 사용)하고 아래로 사라지면 인스턴스가 하나 감소(table.remove 사용) 하는 방식으로 동작합니다. 운석을 생성/갱신/소멸시키는 루틴은 외부모듈고 작성하고 main.lua에서는 이것을 단순히 호출하여 실행합니다. 이런 식으로 작성하면 게임 (다른 장르일 수도 있구요) 의 예를 들어서 몬스터들, 총알들, 정보표시객체들 등등으로 구분해서 각각을 별개의 모듈로 관리하여 훨씬 효율적으로 전체를 짜맞출 수 있습니다. 제가 개인적으로 사용하는 방법이라 물론 정답이 아닐 수도 있습니다. 각자만의 방법을 찾아보는 것도 재미있을 것 같습니다.

  main.lua와 ast.lua화일은 아래와 같고 소스는 첨부로 붙였습니다. ast.lua화일은 제 이전 포스트를 이해하셨다면 읽어 내려가는데 큰 무리는 없을 겁니다.


"main.lua"
┌─────────────────────────────
local CAst = require "ast"
local Rand = math.random

CAst.LoadImage() -- 이미지 로드

local function Update()
if Rand(0,10)==1 then -- 임의로 운석 생성
CAst.New()
end
CAst.Update() -- 운석의 위치를 업데이트 하는 함수 호출
CAst.ShowInfo() -- 콘솔창에 정보를 표시하는 함수
end

Runtime:addEventListener("enterFrame", Update)
└─────────────────────────────

"ast.lua"
┌─────────────────────────────
local Rand = math.random
local tIns = table.insert
local tRmv = table.remove

-- 멤버 함수로 붙일 메타테이블 : __index 구현
local mtIndex = {}
function mtIndex:Update()
self.img.x = self.img.x + self.nSpdX
self.img.y = self.img.y + self.nSpdY
if self.img.y > 550 then
self.bHaveToDispose = true
end
end

-- 외부에서는 접근하지 못하는 지역변수들
local tInstances = {} -- 인스턴스를 저장할 테이블
local sht — 이미지를 저장할 변수

-- 외부에서 접근할 수 있는 클래스 구현부
local M={}
function M.LoadImage() -- 정적함수 이미지를 메모리로 로드
sht = graphics.newImageSheet("ast.png",
{frames = {
{x=0, y=0, width=125, height=125}, -- frame 1
}
})
end

function M.DisposeImage() -- 정적함수 이미지를 메모리에서 제거
sht = nil
end

function M.ShowInfo() -- 정적함수 정보 표시
print("number of instances: "..#tInstances)
end

function M.Update() -- 정적함수 전체 인스턴스들 갱신
for id=#tInstances,1,-1 do -- 전체 인스턴스들을 *역순*으로 검색한다.
local ast = tInstances[id]
ast:Update()
if ast.bHaveToDispose then
ast.img:removeSelf() -- 이미지를 지우고
tRmv(tInstances, id) -- 테이블에서 인스턴스를 제거
end
end
end

function M.New() -정적함수 인스턴스 하나를 생성
local img = display.newImage(sht, 1)
img.x, img.y = Rand(0,320), -70

-- 멤버변수 이미지, x축속도, y축속도부울변수
local ast = {
img = img,
nSpdX = Rand(-2,2),
nSpdY = Rand(5,10),
bHaveToDispose = false,
}

-- 멤버 함수를 메타테이블로 붙인 후 반환
setmetatable(ast, {__index = mtIndex})
table.insert(tInstances, ast)

return ast
end

return M
└─────────────────────────────

코로나(루아)에서 클래스를 구현하는 간단한 방법 (1)

  잘 알려져 있다시피 클래스(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
└─────────────────────────────

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

메타테이블의 __index 필드에 대해서

  루아에서는 테이블에 메타테이블(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이 찍힌다




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

루아(코로나)에서 테이블을 다룰 때 주의할 점 (1)

  루아(코로나)의 테이블의 요소(field)는 숫자를 키(key)로 가지는 것들과 문자열을 키로 가지는 것들로 구분할 수 있다. 예를 들어서

          local tA = { 11, x=160, “hello", 35, y=200, true}

위 테이블은 숫자키를 가지는 요소가 색으로(굵게) 표시된 것들이며 아래와 완전히 동일하다.

          local tA = {
                    [1]=11, -- 숫자키
                    [“x”]=160,
                    [2]=“hello", -- 숫자키
                    [3]=35, -- 숫자키
                    [“y”]=200,
                    [4]=true, -- 숫자키
          }
즉, 숫자키를 가지는 요소는 네 개이고 문자열키를 가지는 요소는 두 개다. 특이한 것은 루아는 아무 키도 지정하지 않은 요소는 자동으로 숫자키가 1부터 시작해서 할당되며, 또한 사용자가 아무 숫자로 키를 지정할 수 있다는 것이다. 0으로 지정할 수도 있고 음수도 가능하며 심지어 실수도 가능하다.

          local tB = { [-10]=11, 21, 33, x=50, 44 }

  한 가지 오해하기 쉬운 것은 위의 예에서 두 번째 요소인 21의 숫자키가 무엇일까 하는 것이다. -9라고 생각하기 쉬운데 1이다. 앞에서도 언급했듯이 아무 키도 지정되지 않는 요소는 항상 숫자키 1로 부터 시작해서 1씩 증가시킨다.

          local tC = { [2]=11, 22, 33, 44 }

위의 예에서 11의 숫자키가 2로 지정되었는데 33도 2가 되서 숫자키가 겹치게 되므로 보통은 이렇게 정의하지 않을 것이다.

  주의할 점은 테이블의 크기를 구하는 #연산자를 사용할 때이다. #연산자는 자연수(즉, 양의 정수 1,2,3, ...) 숫자키를 가지는 요소만 고려한다는 점에 유의해야 한다. 따라서 앞의 예에서 #tA 값은 4이고 #tB 값은 3이다. 다른 예를 들어 보면

          print( #{ 11, 22, “abc", 44} ) -- 4가 찍힌다.
          print( #{ 11, 22, "abc", x=4} ) -- 3이 찍힌다.
          print( #{ [0]=11, 22, "abc", x=4} ) -- 2가 찍힌다.

  좀 더 정확히 얘기하면 루아에서 #의 정의는 '자연수키를 가지는 요소 중에서 [n]번 요소는 nil이 아니고 [n+1]요소가 nil일 때의 n값(들)' 이다. 테이블을 자연수키로만 얌전하게(...) 관리한 경우 #연산자로 그 크기를 정확하게 구할 수 있다.

  혼동의 여지가 있는 경우는 중간에 nil이 섞여 있을 때이다. 이 경우는 #연산자로 정확한 크기를 구할 수 없다.

          print( #{ 11, 22, nil ,44 } ) -- 4가 찍힌다.
          print( #{ 11, 22, nil, 44, 55, nil } ) -- 2가 찍힌다.

또한 for와 ipairs()함수를 조합하면 테이블의 자연수키 요소만을 고려하게 된다. 그런데 이 경우에도 nil이 나오면 거기서 멈춘다는 것에 주의해야 한다.

          local tE = {11, 22, x=33, 44, nil, 55}
          local cnt = 0
          for key, value in ipairs(tE) do
                    cnt = cnt + 1
                    print(key.." : ".. value) -- 1:11, 2:22, 3:44 까지만 찍힌다.
          end

위의 예에서 문자열키를 가지는 x=33는 건너뛰고, 44까지는 가는데 그 다음의 nil 때문에 55까지는 도달하지 못하게 된다. (이에 반해서 pairs() 함수는 nil을 제외한 숫자키와 문자열키를 가지는 요소 모두를 고려한다.)
  코로나로 코딩을 하다보면 다양한 객체들 (이미지, 타이머, 애니메이션 등등)을 테이블에 저장하여 배열로 관리하는 경우가 있을 것이다. 예를 들어서 다음과 같은 방식이다.

          local tTimer = {}
          local tSprite = {}
          ...
          tTimer[#tTimer+1] = timer.performWithDealy( 500, SomeFunc ) -- 테이블에 타이머레퍼런스 저장
          tSprite[#tSprite+1] = display.newSprite( someSheet, someSequence ) -- 테이블에 애니메이션 저장
          ...

이런 식으로 배열에 타이머나 애니메이션을 저장하면서 관리하다가 어떤 조건에 따라서 중간의 요소를 삭제하기도 할 것이다.

          ...
          timer.cancel( tTimer[some_Id] )
          tTimer[some_Id] = nil
          ...
          tSprite[ another_id ]:removeSelf()
          tSprite[ another_id ] = nil

이런 경우 위에서 언급한 내용을 고려해서 테이블의 크기를 구하거나 반복문으로 테이블에 저장된 요소들을 검사할 때 논리적인 오류가 발생하지 않도록 해야 한다. 테이블의 요소를 완전하게 제거하려면 table.remove() 함수를 사용해야 한다.