TAF로 프로그래밍하기
일단 TAF를 완성하긴 했습니다. 이제 남은 건 API개선과 기능 고도화 뿐입니다.
하지만 Tree와 Flow로 개발한다는 의미부터 정립하고 가야 TAF가 쓸모 있을 것 같아 입문자를 위한 튜토리얼을 만들어 보기로 했습니다.
현재 사내에서는 ‘역할모델에 기반한 OOP프로그래밍’ 또는 ‘흐름제어에 중심을 두는 구조적 함수 프로그래밍’ 모델만 사용합니다. 첫번째 모델은 복잡한 어플리케이션을 작성할 때 사용하는 방법론이고 두번째는 배너나 단순한 작업을 진행할 때 사용합니다. 하지만 TAF의 개발방법론은 전혀 다른 스타일입니다. 그래서 하나의 예를 들어 TAF스타일로 어떻게 달라지는 지 연습해보겠습니다.
전통적인 역할모델 개발론적용
기존의 게임 개발 시 사용되는 역할모델 방법론을 간단히 생각해보겠습니다. 이 모든 예제에서 사용될 임의의 어플리케이션인 비행기 게임을 소개합니다.
![]()
3D는 아니고 걍 소실점만 살짝 변형 시킨 2D게임입니다. 상하좌우로 움직이며 총알을 쏘고 피하는 간단한 게임입니다. 이제 이 게임을 만들기 위한 역할을 모델링 해보겠습니다.
![]()
간단히 말해 내가 있고 적을 관리하는 녀석과 총알을 관리하는 녀석이 있다는 것입니다.
관계 매핑으로 모델링 심화
1차적인 역할모델이 정의되면 그 다음엔 역할 간의 관계 매핑을 통해 숨겨진 메서드를 찾아냅니다.
상세역할1
CairPlane
fire() – CshotManager 에게 새로운 총알 생성을 의뢰한다.
CenemyManager
update() – 내부에 생성된 enemy들이 fire()를 실행하게 된다.
도출되는 메서드
CshotManager.add( requester:AbstractShooter ); – 총알매니저는 화면의 충돌처리 대상이 누군지 다 알아야 한다.
CshotManager.fire( requester:AbstractShooter ); – 발사 의뢰한 대상이 누군지 알려준다.
상세역할2
CshotManager
update() – 비행기나 적매니져에게 총알이 충돌한 경우 보고해야한다.
도출되는 메서드
CairPlane.hitted( damage )
CenemyManager.hitted( damage)
두 클래스가 AbstractShooter로 부터 구상되었으므로 CshotManager는 구분없이 그저 hitted를 호출하면 그만
도출되는 클래스
CairPlane과 CenemyManager는 Sprite를 직접 상속하지 않고 AbstractShooter::Sprite를 상속해야한다.
AbstractShooter – CshotManager가 충돌체크 후 충돌을 보고할 콜백 인터페이스를 내장하고 있다.
-hitted( damage:int ) – 총알을 맞은 경우 데미지를 받아온다.
-getType() – CshotManager입장에서 requester가 적인이 아군인지 확인해야 충돌처리가 가능하다.
결과
![]()
최초 역할에서 정의된 메서드가 public으로 정의되어 흐름제어 주체인 Main이 호출하게 되고 2차적으로 도출된 속성은 자기들 끼리만 통신하면 되니까 비행기, 적 매니저, 총알 매니저가 같은 패키지라면 internal로 정의될 겁니다.
이미 알고리즘이나 속성을 정하지 않아도 이미 저 상태에서 개발은 거의 끝난 상태가 됩니다. 사실 어려운 건 위 그림에 나오는 개별 함수의 구현이 아닙니다. 위의 다이어그램을 도출해 내는 게 어려운 거죠.
이 예는 매우 전형적인 역할모델 프로그래밍입니다. 역할모델은 익숙해지는데 시간이 많이 걸리고 OOP패러다임에 대한 깊은 이해를 요구합니다(마루타를 이용해 실험 해 본 결과 더욱 그렇다고 느끼고 있습니다^^)
전체 흐름제어
역할정의가 끝나면 중앙에서 흐름제어를 하는 측의 구현만 남게 됩니다. 흐름제어 측은 역할클래스들의 호스트이기도 합니다. 간단히 의사코드로 흐름제어를 담당하는 Main을 살펴보죠.
![]()
(여담인데 대충 코딩하면 이렇게 알아서 그려주는 에디터 없나요 ^^)
TAF에서 흐름을 정의하기
이제 이 모든 과정이 어떻게 TAF로 바뀌는지 살펴볼 차례입니다. TAF에서는 저 아름다운 OOP세상을 사용하지 않습니다. 왜냐면 많이 훈련된 개발자만 저 역할모델 패러다임을 이용한 OOP프로그래밍이 가능하기 때문이죠. TAF는 매우 직관적인 대상과 이 대상을 통제하는 알고리즘만 등장합니다.
class Main{
public function Main(){
//등장하는 화면요소 정리하기
TAF.treeADD( '@root', { 'title':new Sprite, 'play':new Sprite, 'end':new Sprite } );
TAF.treeADD( 'title', { 'titleBack':titleBack, 'button':new button } ); //타이틀화면
TAF.treeADD( 'play', { 'playBack':playBack, 'player':new CairPlane } ); //플레이화면
TAF.treeADD( 'end', { 'endBack':endBack, 'button':new button } ); //종료화면
//필요한 흐름 정리하기
TAF.flowADD( { 'goTitle':goTitle, 'goPlay':goPlay, 'goEnd':goEnd } ); //화면이동용
TAF.bindClick( {'title button':'goPlay', 'end button': 'goTitle' } );//화면이동 버튼의 바인딩
TAF.flowADD( {'left':left, 'right':right, 'up':up, 'down':down } ); //플레이어의 이동처리용
TAF.bindTree( 'play player':[ 'left', 'right', 'up', 'down'] );//플레이어 바인딩
TAF.flowADD( 'hit', shotHit ); //총알의 충돌처리
TAF.flowADD( 'factory', enemyShotFactory ); //적과 총알 생성기
TAF.flowADD( 'move', enemyShotMove ); //움직임처리
TAF.bindTree( 'play', ['factory', 'hit', 'move'] );//play화면과 바인딩
TAF.flowADD( 'isEnd', isEnd ); //사망처리
TAF.bindTree( 'play player', 'isEnd' );//player와 바인딩
TAF.flowADD( 'check', ['factory', 'hit', 'isEnd', 'move'] ); //프로세스로 묶음
TAF.bindRender( '@root title' ); //이 시점에서 타이틀이 노출됨!
}
}
전체 어플리케이션을 위한 설정을 전부 해줍니다. 마지막 줄에서 최초 화면을 로딩하게 되죠. 그럼 그 타이틀에 붙어있는 버튼에 바인딩 된 goPlay라는 흐름의 실제 구현을 보죠.
private function goPlay( $tree:TAFtree ):void{
TAF.unbindRender( '@root title' ); //렌더링에서 title을 제거하고
TAF.bindRender( '@root play' ); //play를 렌더링 시작한다.
TAF.bindLogic( 'check' ); //로직으로 적과 총알 생성기, 충돌처리 및 사망 확인 등을 로딩한다.
TAF.bindKey( {'left':'left', 'right':'right', 'up':'up', 'down':'down' } );//키보드를 매핑한다.
}
위의 흐름을 잘 보면 결국 사망처리를 담당하고 있는 흐름은 isEnd입니다. 이 함수를 살펴보기 전에 bindTree를 이해할 필요가 있습니다.
- isEnd는 위의 소스에서 player와 직접 바인딩 되어있습니다.
- bindTree로 연결된 흐름은 인자로 해당 Tree를 받아옵니다.
- 따라서 isEnd의 인자로 오는 $tree에는 player가 들어오는데,
- player의 실 객체를 감싼 TAFtree형으로 들어옵니다.
- TAFtree는 getWorker() 를 통해 실 객체에 접근할 경로를 제공합니다.
해서 아래와 같은 간단한 함수로 정의할 수 있습니다.
private function isEnd( $tree:TAFtree ):void{
if( $tree.getWorker().isDead() ){
TAF.unbindKey(); // 모든 키연결을 해지하고
TAF.unbindLogic( 'check' ); //로직을 제거하고
TAF.unbindRender( '@root play' ); //렌더링에서 play을 제거하고
TAF.bindRender( '@root end' ); //end를 렌더링 시작한다.
}
}
이 결과 플레이어가 죽었는지 체크하여 end화면으로 넘기게 됩니다.
결론
TAF프레임웍은 코딩양을 줄여주는 목표를 갖고 만들어지지 않았습니다. 어려운 역할모델 설계를 할 필요없게 만들어주는 게 목표입니다.
따라서 전통적인 설계에 대한 대안으로 단순한 알고리즘과 실제 일할 일꾼들을 정의하고 이들을 묶는 것으로 어플리케이션이 작동하도록 하려 합니다. 아직은 과도기고 실제로 이게 더 생산성이 높은지는 실무에서 보다 검증을 거쳐봐야 확신할 수 있겠지만, 역할모델 OOP개발은 분명히 교육기간이 너무 오래 걸립니다.
TAF가 대안이 될지 안될지는 몰라도 계속 시도 해 볼 만한(기업의 차원에서 ^^) 카테고리입니다.
관련된 글:
일단은 개발 과정자체에서 공통된 인터페이스를 뽑아내서 세부 기능을 템플릿처럼 공개해 사용한다는 개념인듯..
OOP 특성은 숨기고 절차형 특성은 오픈하는게 키포인트인것 같은데요 맞게 이해한건지…
개발이란게 항상 특수상황이 생기게 마련이라서 템플릿 코드(TAF메서드)를 쉽게 오버라이딩할수있도록 하는 방식정도만 지원되더라도 정말 효율적인 개발방식이 되겠네요.
각 TAF메서드들을 비헤이비어 역할을 하는 클래스들 간주하고 내부적으로 팩토리 메서드같은 구조로 가져가서 실행코드를 직접 수정할 수있는 여지를 남겨 두는게 확장에 도움이 될거 같은데요..
맞게 이해한건진 몰라도 재밌어서 남겨본 글이니 맞지 않는 내용이면 그냥 무시하세요.^^
아주 정확하십니다 ^^
의사 코드를 넘어 실제 TAF 구현은 광범위한 수비를 하는 팩토리객체와 바인더, 정의된 알고리즘을 제공합니다.
특히 as3에 대한 동적 파싱을 지원해서 as3 소스 자체를 문자열로 전달하는걸 허용하기 때문에 간단하고 검증된 알고리즘의 경우 외부 설정파일로 빼내는 것도 허용합니다 ^^
당연하지만 TAF를 도입한다고 하더라도 결국 저 다이어그램은 그려야겠어요.
분명히 흐름도는 그려야합니다. 그건 피할 수 없지만 흐름도만 그리면 객체다이어그램을 그리지 않을 수 있습니다.
TAF의 다이어그램을 다음 포스트에서 한번 그려보겠습니다만, 미리 말씀드리면 TAF다이어그램은 흐름도에 tree가 바인드 되어있는 형태로 생겨있습니다.