마우스, 키보드의 인터렉션 최적화 1/2
얼마 전 용호님이 뭔가 물어보셨습니다. 그 때 깨달았던 점이 OS수준의 이벤트를 수신할 수 없는 플래시에서는 입력관련 최적화도 상당히 중요하고 또한 이에 대한 토론이 별로 되어있지 않다는 점이었습니다.
저야 게임을 많이 만들다 보니 플래시 내에서 키보드, 마우스에 대한 최적화를 항상 고민하는 편입니다. 그에 관해 간략히 몇 가지 포인트만 살펴보고 나중에 입출력 클래스를 공개하도록 하죠.
마우스
마우스 이벤트에 대해서는 아주 깊이 이해할 필요가 있습니다(사실 플레이어가 일으키는 이벤트는 개별적으로 다 깊이 이해할 필요가 있습니다)
- 마우스의 액션은 플래시플레이어가 수신하여 swf 바이트코드에게 직접 dispatch해준다.
- 이 때 dispatch할 대상에 대한 자동적이 조건이 있다.
- 먼저 interactiveObject 인 녀석들 중
- Stage나 그 하위 트리 중 어딘가에 등록되어 있어야 하며,
- mouseEnabled 속성이 true여야 한다.
이러한 조건이 충족되면 AVM2는 OS에게 수신한 마우스 이벤트를 해당 객체들에게 dispatch합니다. 따라서 우리 눈에 안보이지만 아래와 같은 코드가 이미 클래스에 내장되어있다고 볼 수 있습니다.
class InteractiveObject extends DisplayObject{
public function InteractiveObject(){
addEventListener( Event.ADD_TO_STAGE, ADD_TO_STAGE );
}
private function ADD_TO_STAGE( $e:Event ):void{
removeEventListener( Event.ADD_TO_STAGE, ADD_TO_STAGE );
//AVM2를 직접 호출할 수 있다고 가정한다면
//내부적으로 stage에 등록된 순간 자신의 mouseEnabled값으로 판단하여
//마우스이벤트를 AVM2에 등록한다.
if( mouseEnabled ){
AVM2.addEventListener( MouseEvent.CLICK, bridge );
AVM2.addEventListener( MouseEvent.MOUSE_DOWN, bridge );
…//마우스이벤트 전부
}
}
private function bridge( $e:MouseEvent ):void{
//마우스이벤트를 AVM2로부터 수신하여 이를 자신의 리스너에게 다시 dispatch한다.
dispatchEvent( new MouseEvent( $e.event ) );
//물론 마우스 이벤트를 이렇게 할 수는 없다. AVM2가 내부적으로 한다고 가정하자.
}
즉 조건이 갖춰지면 AVM2는 마우스 이벤트가 생길 때마다 dispatch를 하고 있고 그걸 다시 개별 객체들은 자신의 리스너에게 dispatch하고 있는 셈이죠. 사실 이를 상속한 DisplayObjectContainer의 속성 중 mouseChildren 속성이 있다는 사실을 고려하면 더욱 심각한 상황이 됩니다.
class DisplayObjectContainer extends InteractiveObject{
public function DisplayObjectContainer(){
addEventListener( Event.ADD_TO_STAGE, ADD_TO_STAGE );
}
private function ADD_TO_STAGE( $e:Event ):void{
//상동
}
private function bridge( $e:MouseEvent ):void{
dispatchEvent( new MouseEvent( $e.event ) );
//자식들에게도 전파한다.
if( mouseChildren ){
for( var i = 0, j = numChildren ; i<j ; ++i ){
if( getChildAt(i) is InteractiveObject){
//마우스이벤트를 전파
}
}
}
}
위의 브릿지에서 할 일을 addChild시에 addEventListener했던 bridge에서 했던 별로 상관없습니다(짜피 추측된 코드 ^^) 암튼 논리적으로 볼 때, 저런 일은 어딘가엔 반드시 삽입되어있습니다.
이 추측된 구조로부터 몇 가지 최적화 포인트를 찾아낼 수 있습니다.
- 먼저 AVM2가 dispatch할 대상을 줄여 줘야합니다. 이는 꼼꼼히 Sprite등을 생성하실 때, 마우스 이벤트와 관련 없는 경우 mouseEnabled를 false로 해주시면 달성할 수 있습니다. 원리는 당연한 거 아닌가요 ^^; AVM2가 마우스이벤트를 통보할 대상이 없을 수록 부하는 적어지는 거죠.
- mouseChildren 속성을 생각해보면 AVM2는 stage에게만 마우스 이벤트를 통지하고 stage가 자기자식들에게, 그 자식들이 또 자식들에게 전파하는 구조일 가능성이 높습니다. 따라서 전파의 싹을 막아버리는게 상책입니다. stage.mouseChildren = false는 가혹하지만 트리상 상위에서 막을 수록 전파를 크게 막을 수 있습니다.
- 어쨌든 어떤 객체가 마우스 이벤트를 받았는데 더이상의 전파가 필요없다면 습관을 e.stopPropagation() 해주도록 들이는 좋습니다. 원래 dispatch의 연쇄는 정말 끝장입니다. 커스텀이벤트 구현해서 dispatch연쇄를 네다섯번만 하시고 getTimer()로 측정해보세요. 같은일을 걍 루프돌리거나 직접 호출 때와 비교하시면 끔찍합니다.
- AVM2가 마우스이벤트를 어느 정도 주기로 dispatch하는지를 인지한다. – 이게 바로 아래서 설명할 내용입니다.
AVM2는 얼마나 자주 마우스 이벤트를 dispatch할까요?
마우스를 클릭하면 즉시 반응한다고요? 혹은 마우스를 움직이면 즉시 반응한다고요? – 거짓말이시겠죠 ^^;
제가 테스트해본 바로는 기계에 따라 차이는 있지만 5~20ms 정도의 딜레이를 두고 dispatch를 합니다. 마우스move이벤트에 setPixel같은걸로 테스트해보시면 마우스의 궤적에 따라 선이 그려지는게 아니라 끊어지는걸 보실 수 있습니다. 그것 자체가 마우스 이벤트가 모두 전달되는게 아니라 적당히 띄엄띄엄 보내준다는 증거죠. click이나 up은 상대적으로 빠른 편입니다. 연속이벤트가 아니라 이벤트가 발생하자마자 통지하기 때문에 대부분 5ms 이하였습니다(사실 이 정도 반응속도로는 너무 느리기 때문에 directInput 기반의 fps 손맛은 전혀 안나죠. 화면 fps를 떠나서..)
여하튼 이 한계 내에서 가장 최적화를 해야하는 이벤트는 바로 mouseMove입니다. 이 이벤트는 어디에 쓰냐면 주로 drag에 사용합니다. drag를 쓸 일이 있냐구요? 수많은 똑똑하신 ux강사분들이 악의 ui라고 깍아내리는 drag…
하지만 구글맵이 불편하던가요. 지오메트리 기반은 drag를 엄청 씁니다. 수많은 타이쿤류 게임, FPS과 전략시뮬레이션과 용호님네 스타플도 드래그를 무지하게 씁니다. 어디 그 뿐인가요. 모든 그리기 프로그램도 사실 드래그입니다. 결국 드래그는 마구 씁니다 ^^;
드래그를 구현하는 방법은 여러 가지가 있지만, 많은 분들이 사용하시는 방법은 down에 move이벤트를 걸고, up에 이벤트를 해지하는 것입니다. 이 방법은 절대 사용하면 안되는 방법이기도 합니다.
일단 move이벤트에 반응하여 뭔가 화면갱신이나 작업처리를 하게 되면 모든 시스템의 처리속도가 AVM2가 move이벤트를 통보하는 주기인 25ms이상으로 느려지게 됩니다. 갑자기 애니메이션은 뚝뚝 끊기고 여러 가지 처리도 느림보가 됩니다. move이벤트가 frameRate와 무관한 속도로 일어난다는 점을 꼭 기억하세요.
이미 이걸 이해하시는 순간 move이벤트에 반응하는 식으로 프로그래밍을 하면 안된다는 점을 이해할 수 있습니다.
그럼 대안은 무엇이냐면 up, down, enterFrame으로 이벤트를 구성하고 enterFrame에서 DisplayObject의 mouseX, mouseY를 이용하는 것이죠. 이 부분은 설명보다 move를 이용해서 구현한걸 enterFrame으로 변환하는 과정을 소스로 살펴보죠.
// up,down,move로 구성한 경우
class test1 extends Sprite{
public test1(){
addEventListener( Event.ADD_TO_STAGE, ADD_TO_STAGE );
}
private function ADD_TO_STAGE( $e:Event ):void{
removeEventListener( Event.ADD_TO_STAGE, ADD_TO_STAGE );
addEventListener( MouseEvent.MOUSE_DOWN, function( $e:* ):void{ addEventListener( MouseEvent.MOUSE_MOVE, MOUSE_MOVE ); } );
addEventListener( MouseEvent.MOUSE_UP, function( $e:* ):void{ removeEventListener( MouseEvent.MOUSE_MOVE, MOUSE_MOVE ); } );
}
private function MOUSE_MOVE( $e:MouseEvent ):void{
trace($e.stageX, $e.stageY);
}
}
// up,down,enterframe, mouseX, mouseY로 구성한 경우
class test2 extends Sprite{
public test2(){
addEventListener( Event.ADD_TO_STAGE, ADD_TO_STAGE );
}
private function ADD_TO_STAGE( $e:Event ):void{
removeEventListener( Event.ADD_TO_STAGE, ADD_TO_STAGE );
_isDown = false;
addEventListener( MouseEvent.MOUSE_DOWN, function( $e:* ):void{ addEventListener( Event.ENTER_FRAME, ENTER_FRAME ); } );
addEventListener( MouseEvent.MOUSE_UP, function( $e:* ):void{ removeEventListener( Event.ENTER_FRAME, ENTER_FRAME); } );
}
private function ENTER_FRAME( $e:Event ):void{
trace(mouseX, mouseY);
}
}
이렇게 소스를 바꾸면 뭐가 달라지는 걸까요? 실제로 mouseX와 mouseY의 값이 갱신되는 속도는 move이벤트와 일치합니다. 하지만 알고리즘 처리는 EnterFrame수준으로 빨라지죠. 즉 마우스의 갱신이 비동기적으로 이루어지게 된다는 겁니다. 마우스move는 절대로 비동기화 해야하는 엄청 느린 AVM2이벤트입니다 ^^;
와..길다. 키보드는 다음에 하겠습니다. ^^;
관련된 글:
와~~ 멋진 글입니다.
덧붙여서 제 생각을 말씀드린다면…..
MouseMove이벤트 대신 ENTER_FRAME 이벤트를 사용하는 부분에서 뭔가 dx, dy를 계산해서 이동처리등을 한다고 할때 -1<dx<1 && -1<dy<1 의 조건을 만족하는 경우에만 이동처리를 하는게 CPU의 부하를 줄이는 하나의 방법이 아닐까 생각합니다.
또 한가지는 만약 이동해야할 대상이 다른 컨테이너에 있는 경우나 뭐 특정 다른 클래스에 있는 경우인데요.
ENTER_FRAME에서 이들 컨테이너나 클래스에 위치값을 직접 조정해줄 수 없기 때문에 이벤트 dispatch를 해야할 겁니다. 이때 중요한 점은 dispatchEvent( new Event( "이벤트명", bubble, cancelable ) )에서 bubble을 반드시 false로 해야한다는 겁니다. 말씀하신대로 bubble가 true인 경우은 디스플레이 객체를 따라 전파를 하기 때문에 이런경우 지양해야겠네요.
생각해보니 차라리 dispatchEvent를 하는 객체는 EventDispatcher를 상속받아 만드는 편이 낳겠어요. 그러면 자식 디스플레이 객체들에게 전파가 없기 때문에 더욱 빠른 이벤트 전달이 가능하겠네요.
이제 입문하셨으니 욕심의 끝은 없습니다. 전에 살짝 말씀드린대로 EventDispatcher의 dispatchEvent는 빠른 루프이긴합니다만 루프를 돌릴때마다 new Event를 할 뿐아니라 각 전파단계에서 예상컨데 다시 new를 할걸로 짐작됩니다(만약 그렇지않고서는 갱신불가능한 속성이 몇가지 있습니다) 그얘긴 아래와 같은 dispatchEvent의 의사코드가 짐작되는데..
for each( var f:function in listeners[eventName] ){
var e:Event = new Event(eventName);
//이벤트흐름과 관련된 여러가지 컨텍스트처리
f.call( null, e );
}
이거 먼가 끔찍하지 않나요. new 비용을 생각하면 ^^; 별거 없으시면 걍 method(); 하시는게 퍼포먼스적으로는 정신건강에 이로우실지도 모르는 일입니다.
플렉스하시다보니 프레임웍 기반으로 사고하시는게 기본적으로 있으셔서 ^^;
생산성, 안정성, 일관성은 어찌보면 퍼포먼스의 배치됩니당. ㅎㅎ 퍼포먼스를 달성해도 여전히 저 앞에 세가지가 더 중요한 경우가 많기 때문에 정말 필요한 곳이 아니면 쓰면 안되죵 ^^; ..그러고보니 스타플은 필요해…
저번에 말씀했던 Cthread와 같은 방식을 사용한것도 등록된 리스너들을 전부 찾는 부담감이 있기 때문이였군요. ^^
아마 위에 소스에서도 자신의 enterFrame을 사용하지 않고 전역 Shape한넘에게 몰아주면 보다 나아질겁니다 ^^
좋은거 하나 배웠습니다. +_+
매번 엄청나게 배우고 가는군요 ㅎ
역시 내공이 다르십니다 +_+d
흐 과찬을 언제나 하시는군요. 감사합니다.
이런 원리가…. 바꿔야 겠군요.
ㅎㅎ 옙. 바꾸셔야합니다.
좋은 글 너무나 잘 보았습니다^^
저는 커스텀 이벤트를 자주 사용하고 있습니다.
근데 글을 보니 자제해야겠군요…
질문이 있는데요.
A.swf가 B.swf를 로드해와서 B.swf의 어느 시점에 이벤트를 발생시키고 싶다면
dispatchEvent를 하기 보다는,
A.swf가 B.swf에 이벤트시 실행시키고 싶은 함수명을 넘기면
B.swf에서 그 함수를 실행시키는게 더 효율적인가요?
일단 말씀해주신게 해깔려서 답변을 드리기 애매합니다만, a가 b를 로드한다고 했을 때 메인은 a고 b는 서브죠.
b의 어느시점이란건 b가 로드된 후 a가 b를 실행했다는 뜻이겠죠?
그럼 b가 디스패치를 했다는 뜻이고 a쪽에 리스너가 있다는 뜻이겠군요 ^^;
즉 질문은 정리해보니 b에게 a의 메서드를 리스너로 넘기고 b가 디스패치하는 모델이었는데 b가 디스패치 하지 않고 받아온 a의 메서드를 그냥 호출하는걸로 바꾸는게 맞는가라는 질문이신거 같습니다 ^^;
사실 그건 상황마다 다릅니다. b.addEventListener( ‘event’, a.hnListener ); 라는 코드가 있다면 아마 말씀하신대로 수정한다면
b.eventListener( a.hnListener ); 정도로 넘기고 b는 내부적으로 받아온 인자를 필요할때 호출한다는 뜻이 됩니다.
저는 개인적으로 이벤트모델보다 그러한 함수객체 모델을 선호합니다. 왜냐면 추가적인 부담도 없고 함수의 형식도 자유롭기 때문입니다.
하지만 분명히 어도비는 이벤트모델을 강력히 권장하고 잇습니다 ^^
퍼포먼스는 확실히 함수객체모델이 좋습니다.
결합도는? 오히려 전용이벤트를 쓰시는 경우엔 a가 b.전용이벤트를 import 해야하므로 결합도가 올라갑니다. 하지만 인자로 함수를 받는 모델은 결국 listener:Function 이란 꼴이 될것이므로 결합도는 없다고 봐야겠죠.
질문이 애매했군요
하지만 정확히 짚으셨습니다^^
결국 이벤트모델보다는 함수객체모델을 더 선호하신다는 말씀이시군요.
이벤트모델은 new Event로 객체를 계속 생성하기 때문에
어찌보면 함수객체모델이 메모리적으로도 더 강력할 수 있겠네요.
제가 이번에 하게 될 프로젝트가 컴퓨터를 하루종일 돌리면서 플래시를 한번도 안끄고
계속 실행해야하는 상황이라서… 이런 메모리적인 부분에 많은 신경을 기울이게 되네요.
답변 감사드리구요.
좋은 글들이 너무 많네요. 감사히 읽겠습니다^^
ㅎㅎ 저도 프로젝트에 조금이나마 보탬이 되었으니 다되면 보여주세요 ^^
어떤분야든 원론적인게 중요하다는 생각을 새삼 하게 되네요..
제가 기초가 너무 부족해서.. 기본을 더 다져야 할 필요성이 느껴집니다…
오늘도 잘 배웁니다…^^
^^ 감사합니다.
와…..감사히 잘 배우고 갑니다
어서오세요~
오옷 다시 한번 배우고 갑니다~
ㅎㅎ 여기저기 보고 계시는군요.
아아아
다이버스타 님과 지돌스타 님의 강좌는 돈을 주고 들어도 아깝지 않을만큼 훌륭하고 알짜만 해주시네요.
볼때마다 많은 걸 배우고 있답니다.
요즘 마우스 이벤트에 관해 최적화를 생각해 보려던 차인데 딱 맞게 읽었습니다 ㅎㅎ;
단순한 코딩처리부터 기법까지 설명해 주신 점 다시 감사드립니다~^ ^
참, 생각해 보니 startDrag는 enterFrame에 기초하여 작동하는 것일지 궁금해 지네요
avm2가 정기적으로 mouseMove를 디스패치하는데 이때 다운상태인지를 검사하여 드래그도 처리합니다.
글쿤요 또 하나 얻어 갔습니다. 감사드립니다’ㅡ’