레이블이 코로나SDK인 게시물을 표시합니다. 모든 게시물 표시
레이블이 코로나SDK인 게시물을 표시합니다. 모든 게시물 표시

2015년 5월 4일 월요일

코로나SDK에서 적을 추적하는 호밍미사일 알고리듬

  본 포스트에서는 코로나SDK에서 적을 추적하는 호밍미시일의 알고리듬에 대해서 설명하도록 하겠다. 대충 그린 아래 그림으로 설명하도록 하겠다.


 현재 프레임에서의 미사일 좌표와 각도를 각각 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변수가 미사일 객체이다.

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

2015년 4월 24일 금요일

[00081] 코로나SDK에서 외부 모듈을 사용하는 방법

  프로그램의 덩치가 조금씩 커지면 특정한 기능은 외부파일(모듈)로 따로 분리시켜야 여러모로 효율적이다. 코로나(루아)에서 외부 모듈은 다음과 같이 사용한다. 먼저 새로운 화일을 생성해서 다음과 같이 작성한다.

┌─────────────────────────────
    local M = {}
    M.a = 10
    M.t = {x=0, y=20}
    function M.Fa()
        print("Fa() called.")
    end
    return M
└─────────────────────────────

혹은 위와 완전히 동일하지만 다음과 같이 작성할 수도 있다.

┌─────────────────────────────
    local M = {
    a = 10,
    t = {x=0, y=20},
    Fa = function()
        print("Fa() called.")
    end,
}
    return M
└─────────────────────────────

이 화일에서 하는 일은 테이블 M을 생성한 후 변수 a, t, Fa를 채워서 반환하는 것이다.
이것을 살펴보면 입력변수가 없는 일반적인 함수의 내부와 동일한 구조이고 위아래에function() ... end가 없는 함수의 본체와 모양이 같다는 것을 알 수 있다.
이제 이것을 "lib1.lua" 화일로 저장한 후(화일 이름은 각자 다를 것이다) 현재 화일 내에서 (예를 들어서 main.lua)

-------------------------------------------------------------------------
local libA = require "lib1"
-------------------------------------------------------------------------

과 같이 읽어들이면 "lib1.lua" 화일에서 반환된 테이블 M이 libA변수에 올라온다. 이제 libA변수로 "lib1.lua"모듈의 변수 a, t, Fa등을 다음과 같이 읽고 쓸 수 있다.

-------------------------------------------------------------------------
local b = libA.a -- 변수 읽기
libA.t.x = 100 -- 변수 쓰기
libA.Fa() -- 함수 호출
-------------------------------------------------------------------------

이렇게 기본적인 사용법은 굉장히 간단한데 한 가지 오해할 수 있는 사항이 있다. 예를 들어서 main.lua 가 다음과 같이 작성되어 있다고 하자.

┌─────────────────────────────
    local libA = require "lib1"
    print("libA.t.x="..libA.t.x)
    libA.t.x=100

    local libB = require "lib1"
    print("libB.t.x="..libB.t.x)
└─────────────────────────────

이것의 두 번째 print()문의 출력이 무엇일지 짐작해 보자. 필자는 처음에는 당연히 0일 줄 알았는데 이것은 새로운 테이블 M을 생성해서 libB에 할당하는 것으로 오해하기 쉽기 때문이다. 그런데 실제 실행결과는 100이 찍힌다. 즉 libA의 변경결과가 libB에 반영이 되는 것이다.
이것으로 짐작할 수 있을텐데 lua에서는 같은 외부모듈을 다시 읽어들일 때는 이 전에(맨 처음에) 메모리에 올라온 내용을 참조한다. 즉 libB는 새로 생성되지 않고 이전에 올라온 libA와 완전히 같은 곳을 참조하고 있다. 이후로 몇 번을 반복해서 불러오거나 다른 화일에서 같은 모듈을 읽어 올 때도 맨 처음에 생성된 테이블을 참조한다.

심지어 libA를 삭제해도 모듈은 여전히 메모리에 남아 있다는 것도 유의해야 한다.

┌─────────────────────────────
    local libA = require "lib1"
    print("libA.t.x="..libA.t.x)
    libA.t.x=100

    libA = nil
    collectgarbage("collect") -- libA 완전히 삭제
    local libB = require "lib1"
    print("libB.t.x="..libB.t.x) -- 여전히 출력은 100이다.
└─────────────────────────────

이것을 이용하면 프로젝트 전반에서 공통적으로 참조해야하는 변수를 글로벌 변수로 사용하지 않고 하나의 외부 모듈에 모아놓고 관리할 수도 있다.
[#00081]

[00080] 루아(Lua) 언어에 대한 소개

  루아(Lua)는 확실히 간결하고 쉬운 문법 체계를 가지고 있고 그만큼 초보자가 익히기 쉬운 장점을 가진다. 배우기 쉽다는 파이썬과 비교해서도 문법이 더 간결하지만 반대 급부로 기능의 제한이 있는 것 같다.(대표적인 것이 클래스를 구현하는 표준 문법이 없다.)

www.lua.org 에서 다운로드할 수 있음

  하지만 용량이 작고 성능이 좋아서 많은 게임의 내장 스크립트 언어로 사용되어 왔으며 대표적인 예로 블리자드사의 World of Warcrafe 에서 사용된 것으로도 유명하다. 루아 전체를 구현한 소스가 ANSI C 코드로 6천 줄 이하이고 기본 엔진의 용량이 100KB이하라고 한다.

  처음으로 루아라는 프로그램 언어를 접한 계기가 Corona SDK 라는 플랫폼으로 스마트폰 프로그래밍을 공부하면서이다. corona sdk라는 플랫폼은 현재 무료로 사용할 수 있으며 자신만의 앱을 만들어 동일한 소스로 아이폰과 안드로이드폰의 앱을 동시에 빌드하여 업로드 할 수 있다. 사용하는 언어가 루아이므로 네이티브 언어를 사용해서 개발하는 것과는 비교할 수 없이 난이도가 낮아진다는 큰 장점을 가진다.


 필자의 경우도 루아라는 언어에 대한 기초 지식이 전혀 없었음에도 불구하고 이삼일 정도 이 corona sdk 의 사용법을 익힌 후에 내 갤럭시노트에서 우주선이 터치하는 대로 움직이는 프로그램을 작성할 수 있었다. 그 당시에는 신선한 충격이었다. 안드로이드 앱은 자바, 아이폰은 objective-c (지금은 swift)라고 알고 있었는데 이런 툴이 있다니.

물론 크로스 플렛폼이 몇몇 있기는 하다.

  • cocos2d-x (C++)
  • 유니티3d (C#혹은 javascritp)

위의 두 가지가 현업에가 가장 많이 쓰이는 툴로 알려져 있다. 하지만 취미나 호기심에서 앱을 만들어보고 싶다면 앱인벤터나 코로나SDK가 초보자에게 입문용으로 적당하다고 생각한다.
[#00080]

2015년 4월 20일 월요일

[코로나SDK] 클래스 구현의 간단한 예제

내친김에 간단한 예제를 하나 만들어 보았습니다.


이 동영상 보시면 화면 위에서 운석이 생성되면 인스턴스가 하나 증가(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
└─────────────────────────────

[코로나SDK] sin, cos 연산시간 단축시키기

  어떤 좌표점을 회전시키고자 할 때 회전행렬를 곱해주는데 이 때 sin함수와 cos함수를 사용해야 한다. 예를 들어서 좌표점(x1, y1)을 원점을 중심으로 40도 회전시킨 좌표 (x2, y2)는 다음과 같은 식으로 구한다.

--------------------------------------------------------------------
          x2 = x1 cos(40도) - y1 sin(40도)
          y2 = x1 sin(40도) + y1 cos(40도)
--------------------------------------------------------------------

코로나(루아)에서 이 값들은 math.cos(rad) 와 math.sin(rad) 함수를 이용하여 계산할 수 있는데 입력값이 도(degree)가 아니라 라디안(radian)이다. 여러 가지 이유로 보통은 회전각을 도값으로 결정하는데 (예를 들어서 30도, 45도 등등) 이것의 sin, cos값을 계산하려면 이것에 π/180 으로 곱해서 라디안으로 환산시켜야 한다. 이를 위해서 다음과 같이 (변환)상수를 미리 계산해 놓고 sin(30도), cos(-45도)를 구할 수 있다.

--------------------------------------------------------------------
          local _d2r = math.pi / 180
          ...
          local alpha = math.sin(30*_d2r)
          local beta = math.cos(-45*_d2r)
--------------------------------------------------------------------

  그런데 이러한 회전 연산이 한 프레임 안에서 굉장히 많이 일어날 경우 조금이라도 연산시간을 줄이는 것이 유리할 것이다.가장 손쉬운 방법은 math.sin, math.cos함수를 로컬로 정의해서 호출하는 것이다. 예를 들어서 아래와 같이 정의해 놓고 사용하면 수행 시간이 조금이나마 단축된다.

--------------------------------------------------------------------
          local _d2r = math.pi / 180
          local Sin = math.sin
          local Cos = math.cos
          ...
          local alpha = Sin(30*_d2r)
          local beta = Cos(-45*_d2r)
--------------------------------------------------------------------

이렇게만 해도 (필자가 간단하게 실험해 본 결과로는) 15%정도 실행시간이 단축된다.

  시간을 좀 더 단축시킬 수 있는 방법으로 미리 이 값들을 계산해서 테이블(배열)에 저장하는 방법을 생각해 볼 수 있다. 특수한 경우가 아니면 보통 각도는 1도 단위로 많이 계산되므로 예를 들어 -360도부터 360도까지 (원하는 범위는 상황에 따라 다를 것이다.) 미리 계산해 놓는 것이다. 루아는 배열의 인덱스가 음수도 가능하므로 아래와 같이 하면 된다.

--------------------------------------------------------------------
          local _d2r = math.pi/180
          local tCos, tSin = {}, {}
          for deg=-360,360 do
                    tCos[deg]=math.cos(deg*_d2r)
                    tSin[deg]=math.sin(deg*_d2r)
          end
--------------------------------------------------------------------

이제 이렇게 미리 생성해 놓으면 이후에는 단순히 아래와 같이 배열값을 읽어오는 것만으로도 sin, cos계산이 가능하다.

--------------------------------------------------------------------
          local alpha = tSin[30]
          local beta = tCos[-45]
--------------------------------------------------------------------

필자가 테스트해 본 바로는 연산시간이 첫 번째의 경우보다 50%, 약 절반으로 줄어들었다. 이 방법의 단점은 실수각(예를 들어 30.5도 같은)의 연산이 불가하다는 것이고 약간의 메모리를 소비한다는 것이다.

  필자는 이 방법이 훨씬 빨라서 적어도 90%이상은 실행 시간이 감소될 것으로 예상했었는데 단축시간은 그다지 크지 않았다. 그 이유는 내장 수학함수는 미리 컴파일된 라이브러리가 실행되지만(빠르다) 인덱싱은 인터프리팅으로 실시간 수행되기 때문에(느리다) 그 차이가 그다지 크지 않은 것으로 짐작된다.

[루아] table.remove()사용시 주의점

  루아에서 테이블은 마치 배열처럼 사용할 수 있는데 인덱스가 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()가 실행되어도 이후에 검색할 요소의 인덱스는 변하지 않으므로 모든 배열 요소에 대해서 조건검사가 수행이 되게 된다.