블로그 이미지
개발자로서 현장에서 일하면서 새로 접하는 기술들이나 알게된 정보 등을 정리하기 위한 블로그입니다. 운 좋게 미국에서 큰 회사들의 프로젝트에서 컬설턴트로 일하고 있어서 새로운 기술들을 접할 기회가 많이 있습니다. 미국의 IT 프로젝트에서 사용되는 툴들에 대해 많은 분들과 정보를 공유하고 싶습니다.
솔웅

최근에 받은 트랙백

글 보관함

calendar

    1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31    


지난 번 글을 쓴지 3주나 지났군요. 자기가 쓴 코드도 며칠만 지나면 뭐가 뭔지 너무 오래 되서 이전에 어디까지 공부했는지도 잘 모르겠습니다.


지난번 글 대충 읽어 봤더니 chainFunctions() 함수를 분석하면 될 것 같습니다.


이 함수에 전달되는 파라미터를 보겠습니다.


function chainFunctions(exec,options,runParent,noanim)


exec,options,runParent,noanim 이렇게 4개의 파라미터들이 전달 됩니다.


이 함수를 call 하는 anim 함수에서 이 함수가 call 되는 부분을 보죠.


return chainFunctions(function () return transition.to(obj,options) end, options,runParent)


첫번째 exec 에 해당 되는 부분은 function () return transition.to(obj,options) end 라는 함수 입니다. 여기서 obj,option 은 지난 번 글에 서 다뤘죠.




obj는 흰공의 위치이고 options는 빨간점의 위치값입니다.

그러니까 chainFunction 의 첫번째 인자는 흰공이 options 에 설정된 위치까지 움직이는 겁니다.

두번째 파라미터는 뭘까요?  options입니다. 이 options는 빨간공의 위치이죠? 흰공이 빨간공으로 움직이면 그 다음은 그 빨간공 자리에 흰공이 있게 되니까 그 위치가 흰공의 위치값이 될 겁니다. 아마 그걸 다루려고 이 options 값을 전달하는 것 같습니다.

코드를 분석 해 보면 나오겠죠.

세번째 값인 runParent 는 뭘까요? anim() 함수에서도 runParent를 던져주긴 하는데 이제 main 함수에서 call 됐을 때는 전달받지 못한 겁니다.


그럼 뭘까요?



anim() 함수가 main 함수에서만 call 되는 건 아닙니다. chainFunction() 함수 안에서도 이 anim() 함수가 호출되는데요.


return anim(arg[1],arg[2],run) 라는 부분이 있습니다.


여기에 세번째 파라미터가 있죠?

이건 chinFunction() 내부를 분석하면 뭔지 알 수 있을 겁니다.


그리고 chainFunction() 에는 네번째 파라미터가 noanim 이라고 있는데 anim() 함수에서 호출 될 때는 이 값이 없습니다.

자세히 보니까 chainFunction() 함수 내에서 자기 자신인 chainFunction()을 호출하는 부분이 있습니다.


return chainFunctions(arg[1],{},run,true)


이 4번째 파라미터는 true 이군요.

이것도 chainFunctions() 함수 내부 로직을 분석하면 어떻게 사용되는지 알 수 있을 것 같습니다.


들어가기 전에 이 소스를 만든 개발자가 적어 놓은 주석부터 한번 보겠습니다.


각 파라미터 (parameters, arguments) 들에 대한 설명을 달아 줬네요.


-- args
-- exec - is the current function to execute. It is assumed to be a function that runs a Corona SDK transition

exec는 현재 실행되는 함수이다. 이 값은 Corona SDK transition이 실행되는 함수가 전해질 것이다.


-- options - options for the animation (see Corona SDK). These are assumed to be the same closed over by the exec call - chainFunctions relies on being able to manipulate them for "onComplete" and "whenDone" to work

options 는 animation을 위한 것이다. (Corona SDK)를 보세요. 이것들은 chainFunctions에서 exec에 의해 실행 된 다음에 onComplete과 whenDone 이 됐을 때 생성되게 되는 것들이 될 것이다.


--         # Additional feature: if options.delete==true the subject of the animation will be deleted (obj:removeSelf() called) when the animation completes

# 추가 기능들 : 만약 options.delete 가 true 이면 애니메이션이 완료 되면 그 애니메이션 되는 대상은 delete 될 것이다. (obj:removeSelf() 가 call 됨으로서).


-- runParent (optional) - run the previous function in the chain. It takes a single function as an argument. chainFunction creates this function itself

runParent(옵션)은 chain에 있는 이전 함수를 실행한다. argument로 single function을 갖는다. chainFunction 이 이 함수를 생성한다.


-- noanim (optional) - true if exec does not represent a function executing a Corona SDK transition. If true, the only valid call to the chain is "start"

noanim(옵션) - true. 만약 exec 가 Corona SDK 의 transition을 실행하지 않을 때 이 값은 true 가 될 것이다. 그러면 여기서 start 함수만 call 될 것이다.




이렇게 친절하게 주석을 달아 주었네요.


이 주석만 봐도 대충 이 함수의 로직이 이해가 됩니다.


main 함수에서 설정했던 빨간공의 위치들이 돌아가면서 options 가 되서 흰공이 이 빨간공 위치로 계속 이동할 것이고 더이상 빨간공의 위치가 없으면 noamin이 true 가 되서 start 함수가 호출되서 끝나는 거군요.


오늘은 여기까지 하구요.


다음 글에서 제대로 chainFunctions() 부분을 분석해 보도록 하겠습니다.



저작자 표시 비영리 동일 조건 변경 허락
신고


지난번에 이어 animation chain 소스를 분석해 보겠습니다.

지난번 글을 올린지 1주일이 지났나보네요.


지난번 main.lua 에서는 화면에 나오는 빨간 공 4개와 흰공 한개를 배치하는 로직이었습니다.

animationchain.lua 에서는 이 흰공을 4개의 빨간공을 따라서 움직이게 하는 코드가 있을 겁니다.


main.lua 의 마지막 줄에 이 작업을 하기 위해 animationchain.lua 에 있는 함수를 이렇게 call 했습니다.


animationchain.anim(c,stops[1]).whenDone(c,stops[2]).whenDone(c,stops[3]).whenStart(c,stops[4]).onComplete(c,stops[5]).onStart(function() print("done") end).start()


c 가 바로 흰공이었고 stops 에는 빨간공들의 위치값들이 있습니다.


animationchain.lua 의 소스는 아래와 같습니다.


local M={}
animationchain=M

local transition=transition
local setmetatable=setmetatable
local type=type
local error=error

setfenv(1,M)

local function start(anim,options,onStart,onComplete)
    return function()
        if options.delete then
            options.onComplete=function(obj)
                obj:removeSelf()
                if onComplete then
                    onComplete()
                end
            end
        else
            options.onComplete=onComplete
        end
        anim()
        if onStart then
            onStart()
        end
    end
end

-- args
-- exec - is the current function to execute. It is assumed to be a function that runs a Corona SDK transition
-- options - options for the animation (see Corona SDK). These are assumed to be the same closed over by the exec call - chainFunctions relies on being able to manipulate them for "onComplete" and "whenDone" to work
--         # Additional feature: if options.delete==true the subject of the animation will be deleted (obj:removeSelf() called) when the animation completes
-- runParent (optional) - run the previous function in the chain. It takes a single function as an argument. chainFunction creates this function itself
-- noanim (optional) - true if exec does not represent a function executing a Corona SDK transition. If true, the only valid call to the chain is "start"
function chainFunctions(exec,options,runParent,noanim)
    local t={}

    local mt={
        __index=function(t,k)
            local run=function(execChildAnim)
                local doIt
                -- wrap child animation in relation to previous call
                local onStart,onComplete
                if k=="onStart" or k=="whenStart" then
                    onStart=execChildAnim
                elseif k=="onComplete" or k=="whenDone" then
                    onComplete=execChildAnim
                end
               
                doIt=start(exec,options,onStart,onComplete)
               
                if runParent then
                    runParent(doIt)
                else
                    doIt()
                end
            end

            return function(...)
                if k=="start" then
                    return run()
                end
                if noanim then
                    error("animationchain: passing in pure functions must terminate the chain. Only call start after passing a single function into the chain")
                end
                if #arg==1 and type(arg[1])=="function" then
                    -- just a function has been passed in.
                    return chainFunctions(arg[1],{},run,true)
                end
                   
                return anim(arg[1],arg[2],run)
            end
        end
    }

    setmetatable(t,mt)
    return t

end

function anim(obj,options,runParent)
    return chainFunctions(function () return transition.to(obj,options) end, options,runParent)
end

return M


여기서 어느 부분이 제일 먼저 실행 될까요?


이 파일에는 크게 세개의 함수가 있습니다. 맨 처음에 start() 함수가 있구요. 두번째에 chainFunctions() 그리고 세번째로 anim() 함수가 있습니다.


start 함수가 맨 처음에 있고 이름도 start 이니까 이게 제일 먼저 실행 될까요?

아닙니다. 사실은 start() 함수는 이 세개중에 제일 나중에 실행 됩니다.


함수는 running time 중에 누군가가 call 해 주지 않으면 실행되지 않습니다.

마치 옛날 왕의 여자들처럼 나라를 run 하시는 왕이 call 하지 않으면 평생 독수공방해야 되듯이요.


제일 처음 실행되는 함수는 anim() 입니다. 왜냐하면 main.lua 의 맨 마지막 단계에서 이 함수를 call 했기 때문이죠.


이 anim() 함수는 무슨 행동을 할까요?


chainFunctions() 함수를 return 합니다. 즉 chainFunctions() 함수가 실행 되는 거죠.

여기서 파라미터로 function () return transition.to(obj,options) end 와 options, 그리고 runParent가 pass 됩니다.


이 파라미터들이 어디서 온건지 알아볼까요?




그냥 설명하면 길어질 것 같아서 위와 같이 정리해 봤습니다.

c 는 흰공이고 stops[1] 은 빨간공의 좌표 입니다.

빨간공은 따로 파라미터로 넘겨 주지 않습니다. 왜냐하면 흰공만 움직일 거니까요. chainFunctions() 에서 필요한건 빨간공의 좌표값뿐입니다.


우선 main.lua 에서 이 c(흰공) 과 stop[1] 첫번째 빨간공의 좌표를 넘겨 줍니다.

이걸 animationchain.lua 의 anim(obj,options,runParent) 함수가 받죠. 

여기서 obj 는 첫번째 파라미터로서 흰공 c를 말하고 options는 빨간공의 좌표 입니다. 그리고 세번째 좌표 runParent는 main.lua 에서 전달하지는 않았습니다.

이건 나중에 살펴 보겠습니다.


anim() 함수 안에서는 chainFunctions()를 콜하면서 3개의 파라미터를 전달하죠?

그런데 첫번째 파라미터는 함수를 전달해 줍니다.

transition.to(obj,options) 라는 함수 입니다.

transition.to()함수는 corona sdk 에서 제공되는 함수인데 애니메이션 표현할 때 아주 많이 사용합니다.

transition() 함수 API 를 보시려면 여기로 가셔서 참고하세요.


이 transition.to(obj.options)를 해석하자면 obj를 options 까지 움직인다는 의미 입니다. 즉 흰공을 stops[1] 까지 움직이는 거죠.


한번 소스 코드를 바꿔볼까요?


main.lua 에서

animationchain.anim(c,stops[1]).whenDone(c,stops[2]).whenDone(c,stops[3]).whenStart(c,stops[4]).onComplete(c,stops[5]).onStart(function() print("done") end).start()를 없애시고 animationchain.anim(c.stops[1])을 넣어 보세요.


그리고 animationchain.lau 의 anim() 함수 안에 

return chainFunctions(function () return transition.to(obj,options) end, options,runParent) 를 없애시고

transition.to(obj.options)를 넣어 보세요.


그러면 흰공이 가운데에서 왼쪽 빨간 점으로 움직일 겁니다.


main.lua 에서 animationchain.anim(c.stops[2]) 를 넣어 보세요. 

그러면 흰공이 곧바로 왼쪽 위에 있는 빨간공으로 움직이죠?


이렇게 해 보시면 transition.to() 함수의 기능을 확실히 아실 수 있을 겁니다.


일단 오늘은 animationchain.lua 파일에 있는 3가지 함수 중에 main.lua 에서 call 되서 가장 먼저 실행되는 anim(obj.options,runParent) 함수에 대해서 분석해 봤습니다.


이제 이 anim() 함수 안에서 chainFunctions()를 호출 하는 것을 확인 했습니다.

이때 3개의 파라미터가 전달 되는데 첫번째 파라미터는 흰공이 2번 빨간 공까지 transition 한다는 함수이고 두번째 파라미터는 options로서 이건 main.lua에서 부터 전달 된 빨간 공의 좌표 입니다.


runParent는 아직 정체를 알 수는 없습니다.


아마 이 소스의 핵심 부분인 chainFunctions()를 분석하면 알 수 있을 겁니다.


오늘은 여기까지만 하고요.


다음에 chainFunctions()를 분석해 보겠습니다.

저작자 표시 비영리 동일 조건 변경 허락
신고


오늘은 정말 오랜만에 Corona SDK 의 한 샘플 코드를 받아서 공부 좀 해 봤습니다.


animationchain 이라는 소스인데요.


소스는 https://github.com/personalnadir/animation 에 가시면 보실 수 있습니다.


파일을 다운 받아서 실행시켜 보시면 아래와 같은 화면을 보실 겁니다.






화면에 빨간 점이 4개 찍혀 있고 하얀 공이 이 빨간 공들을 찍으면서 움직입니다.

궁금하신 분들은 파일 다운 받아서 직접 실행해 보시면 됩니다.


오늘은 우선  main.lua  파일을 분석해 보죠.


require "animationchain"

local stops={
    {x=20},
    {y=20},
    {x=200,simul=true},
    {y=600},
    {x=display.contentCenterX,y=display.contentCenterY,delete=true}
}

local x,y=display.contentCenterX,display.contentCenterY
for k,v in ipairs(stops) do
    x,y=(v.x or x), (v.y or y)
    if not v.simul then
        display.newCircle(x,y,10):setFillColor(255,0,0)
    end
end

local c=display.newCircle(display.contentCenterX,display.contentCenterY,20)
animationchain.anim(c,stops[1]).whenDone(c,stops[2]).whenDone(c,stops[3]).whenStart(c,stops[4]).onComplete(c,stops[5]).onStart(function() print("done") end).start()


첫번째 줄은 animationchain.lua 라는 파일에 있는 함수를 implement 한다는 얘기 입니다.

오늘은 main.lua 만 다룰거니까 이 animationchain.lua 파일은 신경쓰지 않겠습니다.


다음은 stops 라는 배열에 key,value 형식으로 값들을 집어 넣었습니다.


다음은 x 와 y라는 변수에 화면 중심의 x, y 값을 대입했습니다.


그 다음은 for 문으로 위에 정해 줬던 stops 라는 key,value 쌍으로 돼 있는 배열을 돌면서 원을 만드네요.

화면에 찍힌 4개의 빨간색 점들이 이 for 루프를 돌면서 찍힌 겁니다.


display.newCircle(x,y,10):setFillColor(255,0,0) 는 지름이 10픽셀인 원을 x,y 좌표에 그리는데 그 원의 색은 빨간색 (RGB 에서 R 만 255이기 때문에 빨간색이 됩니다) 으로 그리라는 뜻입니다.

그다음에 또 원을 그리네요 c 라는 변수에 담기는 원을 그리는데요.

20픽셀의 지름을 갖는 원을 그리는데 그 위치는 화면의 중앙이 됩니다.

따로 원의 색을 지정해 주지 않았기 때문에 디폴트인 흰색(255,255,255)이 됩니다.


그 다음은 아까 require 에서 지정한 animationchain 이라는 파일 안에 있는 anim() 이라는 함수를 call 하는 겁니다.


일단 이 부분은 오늘 다루지 않을 부분이구요.


for 문에서 main.lua 의 주요 작업이 다 진행되니 이 for 문을 집중적으로 보겠습니다.

처음에 나오는게 ipairs 함수 입니다.


이 함수는 3개의 값을 return 합니다.

신택스는

for i,v in ipairs(t) do body; end

입니다.


이럴경우 (1,t[1]), (2,t[2]), ..., 이런 값들을 리턴하는데요. 이 루프는 ipairs안에 있는 t 라는 테이블의 마지막에 있는 값까지 도달하면 끝이 납니다.



그럼 main에 있는 for 문을 볼까요?


for k,v in ipairs(stops) do
    x,y=(v.x or x), (v.y or y)
    if not v.simul then
        display.newCircle(x,y,10):setFillColor(255,0,0)
    end
end


for 문만 보면 stops 에 있는 값들의 갯수 만큼 for  문이 돌겠군요.

그리고 그 값들은 1,stops[1], 2,stops[2] 이런식으로 진행이 됩니다.


다음줄은 x 에 v.x 나 x 를 대입하고 y에는 v.y나 y 를 대입합니다.

그러니까 v.x 가 있으면 그 값을 대입하고 그 값이 없으면 위에 지정한 x 값 즉 화면 중앙의 x 좌표값을 대입하게 됩니다.


그러니 처음 x 값은 stops 의 첫번째 값인 20 이 대입될 겁니다. 그 다음엔 y 값은 없으니까 for 문 바로 위에 지정해 둔 화면 중앙의 y 값이 대입될 거구요.


그리고 그 값을 x,y 값으로 해서 지름 10짜리의 빨간색 원을 그릴겁니다.


그 다음은 다시 for 문 처음으로 가는데 x, 값은 아까 지정된 20이 있을 테고 그 다음은 stops[2] 가 선택이 되니까 y 값으로 20이 지정될 겁니다.


그 다음에 두번째 빨간색 원이 그려질텐데 그 위치는 20,20이 될 겁니다.


다음 for 루프 돌 때는 stops[3] 가 되서 x는 200 이 되겠구 y는 따로 없으니까 바로 전에 지정된 20이 되겠네요.


그런데 if 문에서 v.simul이 아닐 경우에만 원을 그리라고 했으니까 이 원은 그려지지 않을 겁니다.


말로 설명하면 잘 감이 안올 수도 있으니까 눈으로 확인해 보겠습니다.


local stops={
    {x=20},
    {y=20},
    {x=200,simul=true},
    {y=600},
    {x=display.contentCenterX,y=display.contentCenterY,delete=true}
}

local x,y=display.contentCenterX,display.contentCenterY
for k,v in ipairs(stops) do
    x,y=(v.x or x), (v.y or y)
    print (v.x )
    print (v.y)
    if not v.simul then
        display.newCircle(x,y,10):setFillColor(255,0,0)
        print (k ..  " : " .. x .. " , " .. y)
        cord = k .. " : " .. x .. "," .. y;
        display.newText(cord,x+10,y, native.systemFont, 20)
    end
end

local c=display.newCircle(display.contentCenterX,display.contentCenterY,20)


for 문 안에서 x, y 좌표를 console 하고 화면에 찍는 코드를 삽입했습니다.

그러면 결과는 아래와 같습니다.




보시면 첫번째 빨간 원은 20,427 이고 두번째는 20,20 입니다. 그리고 세번째는 200,20 이 될 텐데 if 문에 걸려서 이 세번째 원은 그려지지 않을 겁니다.

네번째는 200,600 이고 다섯번째는 stops 의 마지막 값으로 화면 정중앙에 있는 빨간 원이 그려집니다.



그냥 좌표값을 사용해서 그리면 될 텐데 왜 이렇게 복잡하게 할까요?


일단 이렇게 하면 코드의 양을 줄일 수 있고 빨간원이 그려지는 위치를 stops 배열 안에서 정해주면 되니까 나중에 수정하거나 할 떄 편해 집니다.

가독성이 좋고 maintanance 를 용이하게 해 주는 것이죠.


그리고 모바일은 기계마다 해상도가 다른데 이 stops 좌표를 각 해상도마다 따로 만들어 줘서 기계의 해상도에 따라서 다른 좌표를 사용해서 빨간 원을 그리게 할 때도 편리하게 이용할 수 있습니다.


맨 마지막 줄은 animationchain 파일에 있는 anim()함수를 call 하고 있습니다.

이 때 parameter들이 전달 되는데요.


첫번째 파라미터로 c 즉 하얀 원이 전달 됩니다. 이 c 가 움직일 거니까 이 객체를 파라미터로 전달해야겠죠.

그리고 두번째는 stops[1] 이 전달 됩니다. 이 하얀 공이 첫번째로 갈 목적지가 될 겁니다.


목적지에 도달하면 (whendone) 두번쨰로 c,stops[2] 를 인수로 해서 anim 을 call 하고 이 작업을 c,stops[5] 가 될 때까지 할 겁니다.


그 다음에 onStart()를 붙인 것은 transition이 일어나기 전에 이 onStart() 메소드를 실행하라는 의미입니다. 다음 파일안에 이 onStart 메소드가 있어야 되고 그 메소드 안에서 이벤트 대신 target이 pass 되도록 할 수 있습니다.


잘 이해가 안 되시는 분들은 API를 보시면 도움이 되실 겁니다.


여기서는 onComplete() 다음에 호출 되니까 공이 다 돌면 다음 transition 하기 전에 print 'done' 을 하라는 거네요.


그 다음은 animationchain 에 있는 start() 함수를 호출하는 거구요.


여기까지 main.lua 에 대해 공부해 봤습니다.


다음엔 이 오픈소스의 핵심인 animationchain 파일을 분석해 보겠습니다.


저작자 표시 비영리 동일 조건 변경 허락
신고


Posted on . Written by

 

Building on What’s There

 

이제 여러분의 함수가 API 에 있게 됐고 동작도 잘 되고 있습니다. 이제 코딩을 하면서 그 함수를 call 할 수 있는데요, API 의 새로운 기능을 알게 되는 건 기쁜 일이죠.

이제 기존에 string 라이브러리 함수들 중에 약간 문제 있는 것을 한번 살펴 볼까요. gsub() 함수는 개발자에게 쌈박한 기능을 제공하죠. documentation 페이지를 한번 보세요. 아마 이 함수는 기본적으로 search and replace 기능을 제공하는 함수라는 것을 아실 수 있으실 겁니다.

 

local str = "Hello banana"
print( string.gsub( str, "banana", "Corona user" ) )
--Outputs: Hello Corona user

 

이 함수의 한계는 key/value 를 테이블에 담아서 사용하게끔 기능을 제공하지는 않습니다. 아래처럼 말이죠.

local searchreplace = {
   Hello = "Success",
   banana = "Corona user"
}

 

이 기능을 가능하도록 하려면 테이블 내의 entry들에 대해 iterate 하기 위해 루프롤 돌려야 합니다. 그리고 각 entry 마다 gsub() 함수를 부르면 되죠.

local str = "Hello banana"
for k,v in pairs( searchreplace ) do
   str = string.gsub( str, k, v )
end

print( str )
--Outputs: Success Corona user

pairs() 함수는 searchreplace table에 있는 각각의 인덱스들을 identify 하고 그 이름과 값을 return 하는 함수입니다. Lua table 을 사전처럼 다룰 때 아주 유용하게 사용할 수 있겠죠. 루프 안의 내용들은 생각하지 마시고 이제 string replacement 를 어느곳에서나 사용할 수 있는 훌륭한 함수를 하나 만드신 것만 생각하세요.


이제 함수 안에 루프를 넣어 볼까요.

local function gsubEx( str, searchreplace )
   for k,v in pairs( searchreplace ) do
      str = string.gsub( str, k, v )
   end
   return str
end

이 함수는 string-replaceing 루프를 어느곳에서나 재사용할 수 있도록 해 줍니다.

gsubEx( str, searchreplace )
--Outputs: Success Corona user

아주 쉽죠?

 

Improving What’s There


이제 이것들을 다 같이 함치고 gsub() 함수에 override 해 보죠. 우선 gsub() 함수를 변수에 넣어야 합니다. 아까 보았듯이 Lua 에서는 아래와 같이 함수를 변수에 넣는 것을 지원해 주죠.

local gsub = string.gsub

이렇게 하면 이제 gsub 변수를 사용해서 gsub() 함수를 call 할 수 있게 된 거죠. 이제 함수를 저장했으니 우리가 만든 함수를 이 변수에 replace 해서 gsub 함수를 overwrite 하기만 하면 됩니다. 이 방법은 이미 다뤘었죠. 실제 gsub() 함수가 사용하는 같은 함수 파라미터들을 사용할 겁니다.

string.gsub = function( s, pattern, repl, n )
end


이 단 두 줄로 우리가 한 것은 gsub() 함수의 copy 를 가지게 된 것이구요 그리고 우리가 만든 함수로 이것을 replace 시킨 겁니다. 이제 여기에 마음대로 로직을 넣을 수 있습니다. 아래 로직으로 패턴 파라미터가 string 일 때와 table 일 때를 체크하게 됩니다.

if ( type(pattern) == "string" ) then
   --do something
else
   --do something else
end

string.gsub() documentation을 잠깐 보시면 이 패턴 파라미터가 항상 string 인 것만 보실 수 있을 겁니다.

패턴 파라미터가 string 이면 original 함수를 call 하면 됩니다.


if ( type( pattern ) == "string" ) then
   return gsub( s, pattern, repl, n )  --call the original function and return whatever it returns
else
   --do something else
end

패턴 파라미터가 string 이 아니면 즉 table 이라서 key/value 의 쌍을 이루는 값들이라면 아래와 같이 간단한 루프를 돌립니다. 이 루프는 else 구문 안에 만들어야 겠죠.

else
   --do something else (loop over the keys and replace them in the input string 's')
   for k,v in pairs( pattern ) do
      s = gsub( s, k, v )
   end
   return s
end

우리의 new parameter 이름과 패턴을 사용하기 위해 루프를 살짝 바꿔줬습니다.

 

 


Putting it Together

이제 이것들을 모두 다 합쳐 보죠. 우리가 override 한것과 new gsub() 함수는 아래와 같습니다.

local gsub = string.gsub
string.gsub = function( s, pattern, repl, n )

   if ( type(pattern) == "string" ) then
      return gsub( s, pattern, repl, n )
   else
      for k,v in pairs( pattern ) do
         s = string.gsub( s, k, v )
      end
      return s
   end
end

이 코드의 결과는 string.gsub() 함수가 우리가 만든 로직으로 replace 되게된 겁니다. 우리가 만든 로직은 파라미터가 일반적인 거면 original 함수를 call 하고 테이블이면 우리가 만든 로직을 사용하는 거죠. 짠! 이제 우리는 코로나에 우리만의 라이브러리를 추가했습니다.

print( string.gsub( "Hello banana", { Hello="Success", banana="Corona user" } )
--Outputs: Success Corona user

이렇게 하면 코로나에서 제공되는 어떤 API 라이브러리에도 여러분이 원하는 기능을 추가하실 수 있습니다.

 

Warning!

아마 이것 저것 하고 싶은 것들이 많이 떠오르시죠? 새로운 기능을 추가하실 때는 아래 내용들을 유념하세요.

 original 함수 변수에 저장하는 것을 잊지 마세요.
 함수 내에서 그 함수를 call 하지 마세요. 그러면 무한 루프에 빠지게 됩니다.
 여러분이 만든 함수 내에서 original 함수를 call 하는 조건을 반드시 넣어 주세요.

이건 약간 advanced topic 입니다. 그러니 여러번 직접 해 보시고 이 사용법이 익숙해 지도록 하세요. 직접 production-ready 코드에 넣어서 사용하는 것은 위험합니다. 충분히 테스트 하고 완전 숙지 한 다음에 사용하세요. 여기에는 많은 smoke 와 mirror들이 있습니다. 잘 못 하면 쉽게 좋은 기능을 잃어버릴 수 있습니다.

 


Examples

여기 여러분들이 흥미로워 하실 만한 유용한 내용들 몇개 소개해 드립니다.

1. “math” library — adding a function to calculate length:

--returns the distance between points a and b
math.lengthOf = function( a, b )
   local width, height = b.x-a.x, b.y-a.y
   return ( width*width + height*height ) ^ 0.5  --math.sqrt( width*width + height*height )
end

 

2. “math” library — adding a function to clamp values:

--returns a value clamped between a range
math.clamp = function( val, low, high )
   if ( val < low ) then return low end
   if ( val > high ) then return high end
   return val
end

 

3. print() function — overriding print() to not print when running on a device:

--override print() function to improve performance when running on device
if ( system.getInfo("environment") == "device" ) then
   print = function() end
end

 

4. “math” library — adding a function to calculate nearest multiples:

--rounds up to the nearest multiple
math.nearest = function( number, multiple )
   return math.round( (number / multiple) ) * multiple
end

 

References

    Variable number of arguments: http://www.lua.org/pil/5.2.html
    Lua closures: http://www.lua.org/pil/6.1.html
    First class values: http://www.lua.org/manual/5.2/manual.html
    Functions: http://www.lua.org/pil/6.html
    String trim() function: http://lua-users.org/wiki/StringTrim
    String recipes: http://lua-users.org/wiki/StringRecipes
    Author’s blog: http://springboardpillow.blogspot.co.uk/2012/04/sample-code.html 

저작자 표시 비영리 동일 조건 변경 허락
신고


Posted on . Written by




오늘의 guest tutorial 은 Matt Webster 가 제공해 드립니다. Corona SDK 로 모바일 앱을 개발하는 개발자가 되고 싶어하는 친구 입니다. 그는 여러해 동안 닷넷 기반의 웹사이트 프로젝트에 참여했었고 현재는 런던에서 모바일 앱 관련된 웹 서비스를 manage 하고 있습니다. 그는 physics 에 대해 관심이 많고 관련 글들 과 code sample 들을 Corona Code Share 에 올려 공유했었습니다. 그리고 그의 블로그 blog 에도 공유를  하고 있구요. 여기로 가시면 Matt 의 트위터here에 follow 하실 수 있습니다.

Corona SDK 는 간단하고 강력하면서 아주 많은 유용한 API 들이 있습니다. 여러분들이 바라는 특별한 그 무엇이 있기도 하지요. 이 튜토리얼에서는 아래와 같은 것들이 다뤄 질 겁니다.

    1. 기존의 라이브러리에 새로운 기능 추가하기
    2. 기존의 기능을 확장하기

   


Corona API Libraries

Corona SDK 의 주요 기능들은 API 라이브러리를 통해서 제공됩니다. 아래 링크로 가시면 관련 문서를 보실 수 있습니다.

http://docs.coronalabs.com/api/index.html

string 라이브러리를 보면 다음과 같은 기능들을 보실 수 있습니다.

    string.byte()
    string.char()
    string.find()
    string.format()
    etc…


이 기능들은 Lua 에 있지는 않습니다. Objective-C (iOS) 나 Java (Android)에 있는 기본적인 기능들을 hooks 한 거지요.  math, graphics 나 다른 라이브러리들도 마찬가지 입니다.

어떤 라이브러리들은 완전히 Lua 만을 사용하기도 하죠. 예를 들어 코로나의 widget 라이브러리를 들 수 있습니다. Corona Labs는 original source code 도 가능하도록 기능을 제공합니다. Lua 의 룰에 따르기만 하면 개발자들이 그들만의 코드를 implement 할 수 있습니다. 사실  string  라이브러리를 포함한 모든 라이브러리는 Lua 테이블 입니다. (String 은 테이블입니다. 그리고 각 기능들은 그 테이블의 멤버들이죠.)

그렇기 때문에 쉽게 어떤 필요한 것들을 할 수가 있는 겁니다.


A Useful Custom Function


첫번째로 우리가 배울 것은 어떻게 우리가 만든 기능들을 코로나의 APO 라이브러리에 추가하느냐 입니다. 왜 이게 필요할까요? 예를 들어 string으로부터 space를 없애는 아주 유용한 기능을 가지고 있다고 합시다. 대부분의 프로그래밍 언어에서는 이를 위해 trim() 를 사용합니다.



local function trim( str )
   return ( str:gsub("^%s*(.-)%s*$", "%1") )
end



이 함수 내의 내용에 대해 아실려고 할 필요는 없습니다. 그냥 여러분이 이걸 만들었다고 생각합시다. 아주 훌륭한 코드 입니다. 그리고 시작과 끝에 필요없는 space들을 가지고 있는 string에서 그 space들을 없애는 일을 아주 훌륭히 해 냅니다.



print( trim( " Hello World! " ) )
--Outputs: Hello World!



코로나 개발자들이 흔히 자주 하는 일들 중 하나인데요.  이 trim() 함수를 custom Lua module 에 넣는 일이죠. utils.lua 같이요. 그 방법도 괜찮습니다. 그런데 더 기억하기 쉽고 category 에 맞게 이 일을 진행할 수 있습니다. 이 함수는 string 함수입니다. 이 함수를 built in string 함수처럼 접근하도록 하면 어떨까요?


Adding to Corona’s APIs

이 custom trim() 함수가 utils.lua 파일에 들어 있다고 합시다. 모든 동작이 이 파일 안에서 이뤄지기를 원합니다.  이를 위해 utils.lua는 standard require() call 이 사용되는 메모리 안에 있어야 합니다.

require("utils")

utils.lua 에 있는 함수는 아래와 같을 겁니다.



local function trim( str )
   return ( str:gsub("^%s*(.-)%s*$", "%1") )
end



이제 이 함수를 코로나의 string library 에 추가해 봅시다. 그냥 standard table value assignment 를 따르기만 하면 됩니다.


string.trim = trim



이게 다 입니다. 이제 여러분 코드 어디에서든지 이 함수를 call 하시면 됩니다.



string.trim(" Hello World! ")
--Outputs: Hello World!




The Beauty of Libraries



아래와 같이 스트링을 정의했습니다.



local str = " Hello World! "



string 라이브러리에 trim() 함수를 갖고 있을 때의 이점은 이제 우리는 이 함수를 어떤 스트링의 멤버로서 call 할 수 있다는 겁니다.



print( str:trim() )
--Outputs: Hello World!




스트링 라이브러리가 일반적으로 스트링 변수를 다루는 방법이죠.


Lua 101 (Sort of)



Note: Skip to the end of this tutorial for links to some more advanced topics in the Lua language.

Programming in Lua  웹사이트의 Section 6에서는 함수를 “first-class values with proper lexical scoping.” 로 묘사하고 있습니다. 도대체 이게 무슨 소리인지 더 궁금해 질 것 같군요.


간단히 말하면 Lua 에서 함수를 정의할 때 변수처럼 그 이름을 pass 하실 수 있습니다. 여러분들은 함수에 argument로서 이것을 pass 할 수도 있죠. 혹은 나중에 사용하기 위해 변수에 저장할 수도 있구요. 이런 작업은 아마 수도 없이 해 보셨을 겁니다.

어떤 value 로서 함수를 pass 하는 일반적인 instance는 timer.performWithDelay() 함수와 같이 쓰는 방법입니다.



local function HelloWorld()
   print( "Hello!" )
end

timer.performWithDelay( 1000, HelloWorld, 10 )




이 코드는 10번 Hello를 print 하는데 1초에 한번씩 print 할 겁니다. 그 과정을 풀어서 쓴다면.

    1. low-level timer 함수는 1000 밀리세컨드(1초)를 기다립니다.
    2. 이 timer 함수는 HellowWorld() 함수를 call 합니다.
    3. 이 timer 함수는 count 10 에서 1을 뺍니다. 그리고 다시 처음부터 시작합니다.

   


   
Storing Functions in Variables



다른 함수에 파라미터로서 함수를 pass 할 수 있는 것처럼 우리는 함수를 변수 안에 저장할 수도 있습니다. 이렇게 되면 변수를 그 함수처럼 사용하게 되는 거죠.



local helloFunc = HelloWorld
helloFunc()



위의 코드는

    1. helloFunc 라는 변수를 선언한다.
    2. HelloWorld() 함수를 helloFunc 변수의 값으로 할당한다.
    3.  함수처럼 해당 변수를 call 한다.

이런 과정을 거쳐 아래와 같은 글자를 프린트 할 겁니다.

Hello!



Using Functions as First Class Values



이제 우리는 아래 방법들을 알았습니다.

   -  코로나 API 에 함수 추가하기
   -  변수에 함수 저장하기
   -  parameter처럼 함수 pass 하기

코로나의 기능을 더욱 확장하기 위해 이 세가지의 Lua 기능들을 사용할 겁니다.

저작자 표시 비영리 동일 조건 변경 허락
신고


오랜만에 코로나 튜토리얼을 번역해 봅니다.

거르지 않고 꼬박꼬박 해 오다가 이번 TDD 프로젝트에 참여하면서 두달넘게 공부를 하지 못했네요.

오랫동안 모바일 앱을 만들지 못했는데... 이제 모바일 앱 하나 만들고 싶습니다.

 

----------------------------------------------------

 

Posted on . Written by

 

지난주의 "Goodbye Globals!" 튜토리얼에 이어서 오늘은 non-global 메소드 안에서 scene들 사이에서 오디오 파일들을 어떻게 관리할지에 대해 얘기 나누도록 하겠습니다. 대부분의 앱에서 특정 오디오 파일들은 전체 앱내에서 필요로 하는 경우들이 많습니다. 그리고 어떤 오디오 파일은 특정 scene 에서만 필요로 하는 경우도 있구요. 특정 scene에서 오디오 파일이 로드 돼서 다른 scene 에서도 overlap 되는 상황도 필요할 때가 있습니다. 이런 경우 scene 내의 객체들을 cleanup 하는 것과 별도로 오디오 파일을 관리해야 해서 좀 복잡하게 됩니다.

 

이런 경우 어떻게 관리 해야 할까요? main.lua 에서 common sound를 로드하고 그 사운드들에 global 로 처리하도록 할 겁니다. 그러면 어떤 scene 에서도 해당 사운드를 플레이 할 수 있죠. 그런데 왠만하면 이 global 한 객체들을 사용하지 않는게 좋습니다. 그리고 실제로는 글로벌이 아니면서 여러분만의 global space를 생성하는 방법을 알려 드린 지난주 튜토리얼을 이런 경우에도 활용할 수 있습니다.

 

 

 

Creating the “sfx.lua” Module

 

첫번째로 할 일은 오디오를 처리할 모둘 이름을 sfx.lua 로 해서 만듭니다. 처음 시작하는 몇 라인은 아주 기본적인 겁니다. 약간의 세팅과 데이터가 들어있는 myData.lua 모듈을 사용합니다. 이 내용은 지난주에 자세히 다뤘었습니다. 그리고 나서 변수들을 담을 namespace를 생성합니다. 

 

local myData = require( "mydata" )
local sfx = {}  --create the main Sound Effects (sfx) table.
return sfx

 

루아의 required 는 처음에 한번만 실행된다는 것을 기억해 두세요. 처음 시작할 때 어떤 세팅이나 configure를 core code 에서 할 수 있다는 얘기입니다.

 

예를 들어

 

sfx.boomSound = audio.loadSound( "audio/explosion2.wav" )
sfx.bigBoomSound = audio.loadSound( "audio/explosion.wav" )
sfx.killMeSound = audio.loadSound( "audio/idied.wav" )
sfx.missileSound = audio.loadSound( "audio/missile.wav" )
sfx.blasterSound = audio.loadSound( "audio/scifi048.wav" )
sfx.bossSound = audio.loadSound( "audio/scifi026.wav" )
sfx.shieldsSound = audio.loadSound( "audio/shields.wav" )


이제 간단하게 sound에 대해 처리 하는 부분을 만들겁니다. 그리고 오디오 API 에게 각각의 사운드를 로드하도록 합니다. 그리고 그 handle 을 sfx 테이블에 저장합니다. 루아에서는 이 테이블을 dot 이나 bracket syntax로 구현할 수 있도록 제공합니다. sfx.boomSound 나 sfx["boomSound"] 이렇게 두가지 방법으로 처리할 수 있습니다. 이렇게 하면 나중에 sound를 참조할 handle name을 사용할 수 있습니다. 예를 들어 어떤 scene에서 sfx.lua 가 필요하면 아래와 같이 해당 오디오 파일을 플레이 시키면 됩니다.

 

audio.play( sfx.boomSound )

 

더 나아가서 이 메소드를 extend 해서 추가적인 기능을 부여할 수도 있습니다. 아래와 같이 추가적으로 오디오 파라미터들을 셋업 할 수 있는거죠.

 

audio.reserveChannels( 5 )
masterVolume = audio.getVolume()
audio.setVolume( 0.80, { channel = 1 } )  --music track
audio.setVolume( 0.66, { channel = 2 } )  --boss sound
audio.setVolume( 1.0,  { channel = 3 } )  --voice overs
audio.setVolume( 1.0,  { channel = 4 } )  --alien voice
audio.setVolume( 0.25, { channel = 5 } )  --weak explosion

 

sfx 테이블에서 call 할 수 있는 init 함수를 간단히 생성할 수 있습니다.

 

sfx.init = function()
   audio.reserveChannels(5)
   sfx.masterVolume = audio.getVolume()  --print( "volume "..masterVolume )
   audio.setVolume( 0.80, { channel = 1 } )  --music track
   audio.setVolume( 0.66, { channel = 2 } )  --boss sound
   audio.setVolume( 1.0,  { channel = 3 } )  --voice overs
   audio.setVolume( 1.0,  { channel = 4 } )  --alien voice
   audio.setVolume( 0.25, { channel = 5 } )  --weak explosion
end

 

이제 masterVolume sfx 테이블에 저장됐습니다. 그리고 나중에 이런 다른 action들이 필요 하면 그에 맞는 액션을 사용할 수 있습니다.
그리고 다른 trick으로는 여러분 앱의 sound on/off 세팅을 담당할 audio.play 의 버전을 만들 수 있다는 겁니다. 예를 들어 아래와 같이 필요한 사운드를 필요할 때 사용할 수 있습니다.

 

if ( settings.soundOn ) then
   audio.play( "beep" )
end

 

sfx.play 라는 함수를 생성해서 audio.play 와 비슷하게 작동하도록 할 수 있습니다. 이  방법을 사용하면 sound setting을 좀 더 편하게 할 수 있습니다.

 

sfx.play = function( handle, options )

   if ( myData.settings and myData.settings.soundOn == false ) then
      --your settings dictate NOT to play this sound
      return false
   end

   --otherwise, one of three things is true:
   --1. myData.settings is nil. You haven't set up control, so play the sound.
   --2. myData.settings.soundOn is nil. You have settings, but not a soundOn flag.
   --   So, play the sound since you haven't set up control.
   --3. soundOn is true, which means you want to play the sound. So, play it!
   audio.play( handle, options )

end

Loading Scene-Specific Sounds

Storyboard 와 함께 오디오를 사용할 때 만날 수 있는 어려움들에는 아래와 같은 것들이 있습니다.


• 해당 scene의 main chunk에서 사운드를 로딩하면 문제가 발생할 수 있습니다.왜냐하면 reload를 하려면 scene을 remove 한 다음에 recreate을 시켜야 하기 때문이죠.


createScene 이벤트는 scene이 로드될 때마다 로드될 필요가 없습니다. 그리고 큰 사운드가 로드되면 transition이 delay 되게 될 겁니다.


enterScene 이벤트는 매번 fire 됩니다. 하지만 스크린에 해당 scene이 완전히 로드되기 이전에는 사운드가 로드되지 않는다는 특징이 있습니다.

 

위의 enterSceneexitScene은 한 쌍으로 발생됩니다. enterScene은 scene-specific sound를 로드할 최적의 장소일 것입니다. 그리고 나서 exitScene 이벤트에서 audio.dispose()를 사용해서 그것들을 처리하면 됩니다. (이 때 객체를 nil 처리하는 것을 잊지 마세요.)

 

그런데 만약에 앱이 새로운 scene으로 갈 때 sound 가 갑자기 끊기지 않고 계속 play 되어야 하는 상황이면 어떨까요? 다음 scene에서는 이 audio handle에 접근할 수 있는 방법이 더 이상 없습니다. 이 문제점을 해결 하려면 sfx테이블로 sound 를 로딩하시면 됩니다. 그리고 onComplete phase에서 anonymous 함수를 사용해서 dispose 시키면 됩니다.

 

아래 코드를 보세요.

 

local sfx = require( "sfx" )

-- forward declare the handle
sfx.longsound = nil

function scene:createScene( event )
   local group = self.view

   local background = display.newRect( 0, 0, display.contentWidth, display.contentHeight )
   background.x = display.contentCenterX
   background.y = display.contentCenterY
   group:insert( background )

   local function leaveScene(event)
      if ( event.phase == "ended" ) then
         storyboard.gotoScene( "b" )
      end
   end

   local button = display.newRect( 100,100,100,100 )
   group:insert( button )
   button:setFillColor( 255,0,255 )
   button:addEventListener( "touch", leaveScene )

end

function scene:enterScene( event )
    sfx.longsound = audio.loadSound("audio/mirv_missiles_online.wav")
audio.play( sfx.longsound, { onComplete = function()
                                      audio.dispose( sfx.longsound )
                                      end } )

 

end 

 

루아에서는 onComplete 이벤트가 call 할 수 있는 anonymous 함수를 사용할 수 있도록 제공합니다. 여러분은 이 메소드를 오디오 파일이 끝났을 때 dispose 하도록 할 때 사용하실 수 있습니다. sound 가 실제로 사용되기 전에 sfx table에서 sound handle 하도록 미리 선언해둬야 한다는 것을 잊지 마세요.

 

여러분은 모듈에서 오디오를 처리하는 것의 장점을 아실 겁니다. 특히 Storyboard나 다른 scene manager utility를 사용할 때 말이죠. 오디오는 조심스럽게 그리고 제대로 관리 되어야 한다는 사실을 명심 하셔야 합니다. 물론 그 오디오 파일을 dispose 시키는 것도 말이죠. 그렇게 함으로서 메모리 누수를 방지하고 다른 이상한 문제점들을 방지할 수 있습니다.

 

저작자 표시 비영리 동일 조건 변경 허락
신고


Posted on . Written by



수요일 입니다. 다시 FAQ 시간이 돌아왔습니다. 오늘의 주제는 런타임 에러 처리와 안드로이드 퍼미션에 관한 내용들을 다루겠습니다.



1. What about the Runtime Error Message Popup on current Daily Builds?


안드로이드용은 Build 1030부터 그리고 다른 플랫폼들은 Build 1047 부터 적용됐는데요. 런타임 에러가 나면 팝업 메세지 박스가 뜨고 파일과 line number 와 함께 해당 에러를 보여 줍니다. (debug build 로 세팅 됐을 경우에요).  그리고 물론 콘솔에도 에러 정보가 뜨구요. 그러니까 이제는 콘솔 없이도 런타임 에러를 볼 수 있는 기능이 추가 된거죠. Build 1047 부터 여러분 코드에 런타임 리스너를 include 하실 수 있습니다. 이 리스너는 에러를 trap 하고 런타임 팝업 메세지 박스를 뜨지 않도록 할 수 있습니다. 이 때 다른 동작을 행하도록 분기해 버리면 런타임 에러가 발생해도 앱이 멈추지않고 계속 작동하게 할 수 있습니다.

모든 플랫폼과 맥/윈도우 시뮬레이터에서 런타임 메세지 박스는 디폴트로 보이도록 설정돼 있습니다.





2. How do I implement a Lua Runtime Error Listener?


아래 예제를 보시면 런타임 리스너를 어떻게 implement 하는지 보실 수 있습니다. ("unhandledError") 코로나 시뮬레이터나 디바이스에서 이 코드를 실행하면 print statement 의 string 과 함께 concatenating a nil value 에 의해 발생한 에러를 보실 수 있을 겁니다.



local releaseBuild = true   -- Set to true to suppress popup message

-- Error handler
local function myUnhandledErrorListener( event )

    if releaseBuild then
        print( "Handling the unhandled error >>>\n", event.errorMessage )
        display.newText( ">>> ERROR OCCURRED <<<", 30, 1, native.systemFont, 18 )
    else
        print( "Not handling the unhandled error >>>\n", event.errorMessage )
    end
    
    return releaseBuild
end

Runtime:addEventListener("unhandledError", myUnhandledErrorListener)

-- Displays text message in center of screen
txtMsg1 = display.newText( "Runtime Error Test Code", 55, 200, "Verdana-Bold", 14 )

print( "ABC" .. nil )  -- <<<< Lua error here


이 리스너에서 false (default value)가 return 되면 이 리스너 함수를 나갈 때 팝업 메세지 박스가 뜹니다. true 가 return 되면 리스너 함수가 런타임 에러를 처리한다는 의미 입니다. 그래서 팝업 메세지가 뜨지 않습니다. 위 코드에서 releaseBuild 변수는 팝업 메세지가 표시 될지 안될지를 결정하는 변수 입니다.


3. What is the best practices for the Runtime Error Listener?


런타임 팝업 메세지 와 런타임 에러 리스너 기능을 추가한 이유는 진행되는 코드의 상황과 에러를 보여줄 수 있는 툴을 개발자들에게 제공하기 위해서 입니다. 만약에 디버그를 위해서 빌드를 한다면 (iOS에서는 developer mode가 되겠고 안드로이드에서는 debug key 를 사용한 빌드가 되겠죠.) 에러가 발생한 파일과 그 line number 가 포함된 런터임 에러 정보를 보실 수 있을 겁니다. 이 정보들은 팝업 메세지 박스를 통해서 보게 되죠. 만약에 런타임 리스너를 추가하고 이 팝업 박스를 띄우지 않는다고 하더라도 그 에러 정보들을 얻을 수 있습니다. (리스너에 전달된 event table 이나 콘솔창등에서요.)

production release 라면 error type 만 가능합니다. 런타임 리스너를 implement 하시고 에러 팝업을 띄우지 않도록 세팅한 다음 내부적으로 그 에러를 log 하게 되죠. 이 에러 로그로 무엇을 할 지는 여러분이 하기 나름입니다. 그냥 무시할 수도 있고 문제점들을 track 하기 위해 서버로 보낼 로그 파일을 만들수도 있구요. 팝업 에러가 뜨도록 하는 것의 장점은 이 에러 정보가 Google Play 에 전달 될거라는 겁니다. 개발자들은 그 에러 정보를 Google Play 에서 보실 수 있게 되는 거죠. 만약 팝업 창을 띄우지 않게 되면 여러분이 에러 정보를 log 해서 여러분의 서버에 전달하지 않는한 그 에러 정보는 lost 되게 되죠.

디버그 할 때는 대부분 런타임 팝업이 뜨도록 하는 경우가 많을 겁니다. 그리고 앱 스토어에 올릴 때는 그 팝업을 띄우지 않도록 바꾸는 경우가 많을 거구요.


이전 FAQ 에서 언급한 건데요. 에러를 그냥 무시해 버리는 것은 그다지 좋은 방법은 아닙니다. 런타임 에러가 발생하면 앱이 안정적이지 않게 될 수 있습니다. 가능하면 많은 디바이스에서 앱을 테스트 해 보고 에러가 발생하면 이 에러를 없애거나 pcall 을 이용해서 trap 하셔야 합니다.



4. I'm using the Runtime Listener but I'm still getting the Runtime Error popup.


여러분 코드에서 런타임 에러 리스너를 implement 해서 에러 팝업창을 띄우지 않도록 했는데도 계속 팝업이 뜬다면 런타임 리스너가 시작하기 전에 런타임 에러가 일어났을 가능성이 큽니다. 위 예제에서 보여드렸듯이 런타임 리스너 함수는 코드 내에서 정의를 하셔야 합니다. 그리고 그 함수를 가능하면 빨리 enabling 하셔야 합니다. 그래야 에러를 trap 하실 수 있습니다. 런타임 에러 리스너가 enable 되기 전에 일어난 에러들은 당연히 팝업 메세지를 발생시킬겁니다. 일반적으로 startup 시 발생하는 에러는 fix 하기가 쉽습니다. 그리고 trap 작업도 필요 없구요. 터치 이벤트, 충돌 등이 일어날 때 발생하는 런타임 에러들이 trap 해야할 그런 에러들 입니다.



5. What about Android Permissions and runtime errors?

Build 1030에서 안드로이드 Manifest 에서 디폴트 퍼미션을 없앴습니다. 여러분이 필요한 퍼미션들을 추가해야 된다는 얘기죠. Daily Build 샘플 코드 프로젝트와 API 페이지들을 보시면 build.settings 파일 안에 필요한 퍼미션들이 있읍니다.

대개 퍼미션을 빠뜨리면 런타임 팝업과 함께 런타임 에러가 발생합니다. (혹은 unHandledError 리스너가 call 되기도 하죠.) 퍼미션을 빠뜨리는 것은 앱을 릴리즈 하기 전에 뭔가가 테스트 되고 또 fix 되어야 한다는 얘기 입니다. (6번 질문을 보세요.)



6. Which Android Permissions won't generate a runtime error?

어떤 API call 들은 build.settings 파일에 특정 퍼미션이 세팅되어 있지 않으면 조용히 fail 해 버립니다.
아래와 같은 것들인데요.


  • display.capture
  • display.captureBoard
  • display.captureScreen
  • media.newRecording
  • media.newVideo
  • media.save
  • media.show
  • native.webView
  • native.newWebPopup
  • heading( Compass) event
  • location (GPS) event


위의 API들을 사용할 때는 그 코드가 원하는대로 제대로 작동하는지 꼭 확인하셔야 됩니다. build.settings 파일에 이런 퍼미션들이 세팅되어 있는지도 한번 더 확인하는 습관도 좋은 거 같습니다.


각 API 별로 필요한 안드로이드 퍼미션이 무엇인지 보시려면 Daily Build Documents 를 확인해 보세요.

That's it for today's answers. I hope you enjoyed them and even learned a few things!



저작자 표시 비영리 동일 조건 변경 허락
신고



Posted on . Written by



9. Pre-create Physics Bodies



만약 여러분 시나리오에서 여러 physics body 들을 사용하려고 한다면 그 physics body 들을 non-time-critical code 에서 미리 생성해 두는 것이 좋습니다. 어딘가 스크린 밖에 inactive 상태로 두던가 invisible display group 으로 두었다가 필요하면 active 상태로 바꾸면 되거든요.

그냥 physics body 들 몇개를 time-critical code 내에서 생성하는게 그다지 큰 문제가 되지는 않습니다 단지 한 game 사이클에서 10~20 개의 physics body 들을 한꺼번에 생성하는 것을 피하기 위해서입니다. 이렇게 한꺼번에 생성하면 인식할 수 있을 정도로 frame rate 이 skip 이 일어날 겁니다.

그리고 이 방법은 어떤 면에서는 balancing act 라고 얘기할 수도 있습니다. 200개의 physics body 들을 사전에 생성해서 deactivate 시키게 되면 Box2D 세계에서 이것들을 remove하게 됩니다. 하지만 Corona의 메모리에서 remove되는 것은 아닙니다. 그래서 이렇게 극단적으로 많이 사용할 경우에는 그렇게 효과가 크지 않을 수도 있습니다.


10. Utilize Audio “Best Practices”



앱에서 사용하는 음향효과는 항상 non-time-critical code 에서 pre-load 되어야 합니다. 예를 들어 scene 이나 level 이 시작(begins) 되기 전에 말이죠. 또한 이러한 사운드들을 사용 가능한한 범위 내에서 용량을 최대한 줄여야 합니다. 11khz mono (not stereo)는 대부분의 경우에 acceptable 합니다. 유저가 폰이나 태블릿 스피커 혹은 여러 이어폰을 통해서 들을 수 있는 범위에 속하죠. 그리고 간단하게 그리고  WAV 같은 cross-platform 포맷을 사용하세요. 그래서 CPU를 너무 무리하게 일을 시키지 마세요.

그리고 음향효과들은 아래처럼 테이블로 관리하기를 권장합니다. 쉽게 참조할 수 있고 더 이상 필요 없으면 쉽게 처분할 수 있도록 말이죠.



--load these sounds during NON-time-critical code
local soundTable = {
   mySound1 = audio.loadSound( "a.wav" ),
   mySound2 = audio.loadSound( "b.wav" ),
   mySound3 = audio.loadSound( "c.wav" ),
   mySound4 = audio.loadSound( "d.wav" ),
   mySound5 = audio.loadSound( "e.wav" ),
   mySound6 = audio.loadSound( "f.wav" ),
   mySound7 = audio.loadSound( "g.wav" ),
   mySound8 = audio.loadSound( "h.wav" ),
}


위와 같은 구조라면은 playback 은 다음과 같이 간단하게 사용하실 수 있습니다.


local mySound = audio.play( soundTable["mySound1"] )


항상 그렇듯이 특정 음향효과가 더이상 필요하지 않다면 깨끗하게 정리하는 걸 잊지 마세요. 그리고 이 테이블에서도 reference를 깨끗하게 처리하세요.


local ST = soundTable
for s,v in pairs(ST) do
   audio.dispose( ST[s] ) ; ST[s] = nil
end


여기까지가 퍼포먼스 최적화와 관련된 내용입니다. 개발자로서 이 퍼포먼스 최적화(performance optimization) 은 끝없이 꾸준히 해야 될 일입니다. 그리고 항상 best practice를 해야 합니다. 아무쪼록 이 글에서 다룬 몇가지 팁들이 여러분 앱의 퍼포먼스를 boosting 시키는데 조금이라도 도움이 됐으면 합니다. 질문이나 댓글은 언제든지 환영합니다.



이로써 퍼포먼스 최적화 관련 글을 4번에 걸쳐 다 한글로 옮겨 놨습니다.

항상 코딩하면서 적용해보고 적용해보고 이렇게 반복 연습해서 몸에 익혀 둬야할 내용들 같습니다.


저작자 표시 비영리 동일 조건 변경 허락
신고



Posted on . Written by


6. Avoid “ipairs()”

테이블을 iterating 할 때 Lua ipaires() 함수를 너무 과도하게 사용하는 것은 올바른 방법이 아닙니다. 특히 같은 일을 Lua construct 를 사용해서 처리할 수 있을 때는 말이죠.


ipairs() — Discouraged


local t1 = {}
local t2 = {}
local t3 = {}
local t4 = {}
local a = { t1, t2, t3, t4 }

for i,v in ipairs( a ) do
   print( i,v )
end



Lua Construct — Recommended


local t1 = {}
local t2 = {}
local t3 = {}
local t4 = {}
local a = { t1, t2, t3, t4 }

for i = 1,#a do
   print( a[i] )
end


7. Math Performance Comparisons


다양한 수학 함수들이나 프로세스들은 다른 것들보다 빠릅니다. 그리고 되도록 그 방법을 사용하는 것이 좋습니다.



Avoid “math.fmod()” for Positive Numbers
(양수에는 “math.fmod()” 를 피하세요.)


--math.fmod method (discouraged)
local fmod = math.fmod
for i = 1,100 do
   if ( fmod( i,30 ) < 1 ) then
      local x = 1
   end
end

--modulus operator method (recommended)
for i = 1,100 do
   if ( ( i%30 ) < 1 ) then
      local x = 1
   end
end



Multiplication is Faster Than Division
(나누기보다 곱하기가 빠릅니다.)


x * 0.5 ; x * 0.125  --recommended
x/2 ; x/8            --discouraged



Multiplication is Faster Than Exponentiation
(곱하기가 승수계산보다 빠릅니다.)


x * x * x  --recommended
x^3        --discouraged


8. Conserve Texture Memory


Texture memory는 “critical mass”에 도달할 때까지 자주 무시됩니다. 그래서 art assets 들에 어떤 변화를 요구하도록 하는 작업은 어려우면서도 시간이 많이 소요되는 작업입니다.


Texture memory 에는 8비트나 24비트 PNG 이미지들이 있습니다. 이것들은 alpha channel 이 있는 32비트 이미지로 unpack 됩니다. 여기에는 각 이미지들 마다 픽셀들의 rectangular 배열이 있고 색에 대한 4 color 배열 (channels) 가 있습니다. (빨강, 녹색, 파랑 그리고 alpha(RGB+A).


OpenGL 에서 texture들은 (single 이미지이거나 image sheet들 모두 - Power of 2 (PoT) rule 을 따릅니다. 이 의미는 어떤 texture 이든지 next highest Power of 2 로 반올림 된다는 의미 입니다. (2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, …) 그러면 그 만큼 메모리를 차지한다는 의미 입니다. 그러므로 320x480 크기의 이미지와 260x400 크기의 이미지는 모두 512x512 만큼 texture memory를 차지합니다.

이게 딱 보면 뭐 별로 크게 와 닿지는 않을텐데요 실제 메모리 소비량을 한번 계산해 볼까요. 꼭 제곱근(PoT)만이 아니라 4 color channel들도 같이 생각해 봅시다. 이 의미는 texture 배열에 있는 각각의 픽셀은 4 byte 를 필요로 한다는 겁니다. 그리고 이것은 여러분이 생각하는 것보다 훨씬 더 빠르게 추가됩니다.

Image(sheet) sized 350×500:

512×512 (pixels) × 4 (bytes) = 1,048,576 bytes = 1 MB


Image(sheet) sized 514×1024:

1024×1024 (pixels) × 4 (bytes) = 4,194,304 bytes = 4 MB



이 다음 제곱근(PoT)는 어떻게 되죠? texture memory 가 4배가 될 겁니다. 이건 여러분이 일반적인 디바이스와 Retina/HD 디바이스용으로 개발할 때 더 많이 신경 쓰셔야 할 부분입니다. 이 두 디바이스 간의 이미지가 두배의 크기 차이가 있다면 (iPad 와 Retina iPad 같이) 여러분의 모든 이미지들은 두 기기에서 보두 깔끔하고 쌈박하게 유지하기 위해 원본 이미지와 이보다 두배 큰 이미지를 사용해야 합니다. 이 사이즈 크기가 두배라는 것은 texture 메모리가 더블 이상이 된다는 것을 의미하죠. (위의 경우에는 4배), 그리고 일반적으로 Retina/HD 디바이스들은 이전 버전과 비교해서 4배의 메모리가 있는 것이 아닙니다.

그렇다고 여러분 패닉에 빠지지 마시구요. texture memory 가 관리하는데 또 그렇게 생각보다 엄청난 노력이 필요한 것만은 아닙니다. 아래 팁들을 잘 기억해 두세요.

  1.  texture 가 필요하지 않을 때(display stage 에서 사라질 때)는 항상 unload 를 하세요.
  2. 526x600인(screen size) background texture 가 있다면 기 비율에 맞게 448x512 이미지로 만들어서 사용하세요. 512 PoT(제곱근)을 넘지 않도록요. 그리고 코드에서 그 이미지의 width와 height를 원하는 비율로 살짝 scaling 해 주세요. 유저 입장에서는 약간의 크기 변화가 있어도 그렇게 크게 지정을 주지는 않을 겁니다. 특히 작은 디바이스들에서는요.
  3.  가능하면 texture들을 재사용하세요. 그리고 setFillColor() API를 사용해서 tinting을 하세요. 만약에 빨간 사과와 녹색 사과가 있다면 grayscale image로 사과를 만든 다음에 red와 green tint 를 적용하셔서 사용하실 수 있습니다.
  4.  image sheets를 사용한다면 TexturePacker 같은 tool을 사용하세요. 그래서 가장 작은 PoT configuration으로 pack 하셔서 사용하시면 아주 도움이 될 겁니다.



저작자 표시 비영리 동일 조건 변경 허락
신고


Posted on . Written by



2. Avoid Functions as Arguments for Other Functions

루프나 time-critical code 에서 함수를 localize 하는 것은 아주 중요합니다. 그러면 다른 함수들에 파라미터로 사용됩니다. 아래 두 경우를 보세요:



Defined as an Argument of Another Function — Discouraged

local func1 = function(a,b,func) 
   return func(a+b) 
end

for i = 1,100 do
   local x = func1( 1, 2, function(a) return a*2 end )
   print( x )
end


Localized — Recommended

local func1 = function( a, b, func )
   return func( a+b )
end
local func2 = function( c )
   return c*2
end

for i = 1,100 do
   local x = func1( 1, 2, func2 )
   print( x )
end



3. Avoid “table.insert()”

아래 네 경우를 비교해 보죠. 4개 모두 결과는 같습니다. 테이블에 값을 insert 하는 일반적인 방법들입니다. 이 4가지 중 Lua 의 table.insert 함수를 사용하는것은 별로 좋지 않은 방법입니다.



table.insert() — Discouraged

local a = {}
local table_insert = table.insert

for i = 1,100 do
   table_insert( a, i )
end



Loop Index Method — Recommended

local a = {}

for i = 1,100 do
   a[i] = i
end



Table Size Method — Recommended

local a = {}

for i = 1,100 do
   a[#a+1] = i
end



Counter Method — Recommended

local a = {}
local index = 1

for i = 1,100 do
   a[index] = i
   index = index+1
end



4. Minimize use of “unpack()”


Lua unpack() function 도 퍼포먼스 측면에서 그렇게 좋은 하수가 아닙니다. 다행히 같은 결과를 내기 위해 간단하고 빠르게 loop 를 사용할 수 있습니다.



Lua “unpack()” method — Discouraged

local a = { 100, 200, 300, 400 }

for i = 1,100 do
   print( unpack(a) )
end



Loop Method — Recommended

local a = { 100, 200, 300, 400 }

for i = 1,100 do
   print( a[1],a[2],a[3],a[4] )
end




5. Cache Table Item Access

테이블 아이템들을 캐싱하는 것. 특히 루프 내에서. 이 방법을 사용하면 퍼포먼스를 향상시키고 time-critical code를 만들 수 있습니다.





Non-Cached — Acceptable

for i = 1,100 do
   for n = 1,100 do
      a[n].x = a[n].x + 1
      print( a[n].x )
   end
end



Cached — Recommended

for i = 1,100 do
   for n = 1,100 do
      local y = a[n]
      y.x = y.x + 1
      print( y.x )
   end
end




저작자 표시 비영리 동일 조건 변경 허락
신고

티스토리 툴바