이승재, M2 AI코드 개발 생산성 향상 사례, NDC2013

4,306 views

Published on

Published in: Technology
0 Comments
25 Likes
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total views
4,306
On SlideShare
0
From Embeds
0
Number of Embeds
230
Actions
Shares
0
Downloads
0
Comments
0
Likes
25
Embeds 0
No embeds

No notes for slide

이승재, M2 AI코드 개발 생산성 향상 사례, NDC2013

  1. 1. C++에서 극한의 생산성 뽑아내기M2 AI코드 개발 생산성 향상 사례코드 핫로딩과 자체 제작 스크립트 언어넥슨코리아 W엔지니어링팀이승재
  2. 2. 카바티나 스토리 2007~2009데스크탑 히어로즈 2010~2011마비노기2 2011~이승재프로그래머
  3. 3. 어젠다1. 런타임에 코드를 교체하는 기법을 소개하고,2. C++에 찰싹 달라붙는 스크립트 언어를 만들어본 경험을공유합니다.
  4. 4. 작년가을“AI를 맡으세요”“헉”미션: 기존 코드 튜닝, AI 패턴 추가
  5. 5. AI 작업의특징• 외부 코드에 많이 의존한다.• 시간의 흐름에 따라 연속적으로 변화한다.• 자연스러운 행동이란 뭐지?• 군집 행동의 테스트 케이스는 어떻게 하지?유닛 테스트는 어렵겠고…이터레이션 속도 = 생산성!
  6. 6. 이터레이션• 코드 수정• 빌드• 로딩• 테스트이 루프가 1분 이상;;
  7. 7. 일반적인해결책들• 컴파일 시간: PCH, 의존성 감소• 링크 시간: DLL 빌드• 로딩 시간: 로딩 최적화이미 잘 적용되어 있고 내가 손대기 어려운 것들…
  8. 8. 이래선,안되겠어.빨리어떻게든하지않으면….
  9. 9. 1. 실행 중 DLL 교체2. 스크립트 언어 자체 제작
  10. 10. 10:41:13 김주복 [EIAS] 간단히 말하면 플러그인이잖......
  11. 11. 플러그인?• Dynamic Linked Library (DLL)– 실행 시점에 코드를 메모리에 로드한다• DLL 링크 방법– Implicit Link (묵시적 링크)– Explicit Link (명시적 링크)
  12. 12. ImplicitLink• 보통 접하는 DLL 빌드__declspec(dllimport)• 라이브러리 프로젝트 빌드하면 .lib와 .dll 생성됨• 사용하는 쪽에선 .lib을 스태틱 링크한다.• .lib이 초기화될 때 .dll을 찾아서 자동으로 로드한다.– DLL의 생명주기를 통제할 수 없다.– 언로드하거나 교체할 수 없다.
  13. 13. ExplicitLink• HMODULE WINAPI LoadLibrary(LPCTSTR lpFileName); // DLL을 로드한다.• typedef int (FAR WINAPI *FARPROC)();• FARPROC WINAPI GetProcAddress(HMODULE hModule,LPCSTR lpProcName); // 함수 주소를 얻는다.원리는 정말 간단
  14. 14. 문제와 해결책들
  15. 15. 진입점문제extern “C” 로 묶어주지 않으면 이름이 바뀐다!;;네임 맹글링;;노출하는 함수가 많으면 피곤하다. 일일이 GetProcAddress 해야 하므로..함수 시그니처 어긋남을 컴파일러가 감지하지 못한다.
  16. 16. 진입점문제:해결책• 인터페이스 클래스를 라이브러리와 호출부가 공통 참조하고,이것을 상속·구현한 객체를 라이브러리 함수가 리턴하게 했다.• GetProcAddress 로 단 하나의 함수만 불러오면 된다.• COM의 QueryInterface와도 비슷하다.
  17. 17. 외부심볼참조문제• Explicit Link DLL은 EXE의 심볼을 볼 수 없다.• 프로그램의 다른 부분이 Implicit Link DLL로 되어 있다면추가 비용 없이 그대로 사용 가능하다.• 아니라면 좀 어려운 문제.
  18. 18. DLL안전하게해제DLL 해제하기 전에,내부로의 연결을 모두 끊어야 한다. 빠뜨리면 크래시!→ DLL을 해제하지 않게 해서 해결.코드 변경할 때마다 계속 중복 로드하게 함
  19. 19. DLL덮어쓰기로드된 DLL은 덮어쓰기 불가능.. 빌드가 되지 않는다!→ 로드 전에 DLL 파일 이름을 랜덤하게 바꾸어서 해결이름이 비슷한 DLL 파일이 계속 쌓인다!→ 클라이언트 처음 뜰 때 검색해서 삭제
  20. 20. 빌드구성• 릴리스 버전에는 DLL 교체가 필요 없다.• 스태틱 링크가 가능할까?
  21. 21. 빌드구성디버그 계열 빌드 릴리스 계열 빌드라이브러리의 출력 형태 DLL 정적 라이브러리사용하는 프로젝트에 추가 종속성 설정 안 함 링크될 .lib 이름EXPLICIT_LOAD_LIBRARY(“AI.dll”) LoadLibrary(“AI.dll”) 사라짐EXPLICIT_GET_PROC_ADDRESS(h, FuncType, Func)(FuncType)GetProcAddress(h, “Func”)&Func※ 공통: 사용하는 프로젝트에서 참조 프로젝트로 라이브러리 프로젝트를 추가하되, 라이브러리 종속성은 끈다.빌드 순서는 유지하되 Implicit link는 막기 위함
  22. 22. 행복해졌습니다한동안은..
  23. 23. 1. 실행 중 DLL 교체2. 스크립트 언어 자체 제작
  24. 24. 기존AI코드• C++, 스테이트 패턴• AI 종류별 설정 파일이 존재거대한 고정 파이프라인C++이 있고이것을 제어하는 스위치들데이터이 잔뜩 붙어있는 형태
  25. 25. 기존코드의문제• 복잡도가 아주 빠르게 증가한다.– 기존 코드를 최대한 활용하고,필요한 곳에서 분기하도록 만들게 되는 경향이 있다.– 보스는 전용 기능 덩어리..• 디버깅하기 어렵다.– 이 설정 항목의 의미는 뭐지?– 얘는 왜 이렇게 행동하지?
  26. 26. 대안: 스크립트로이식하자• 거대 고정 파이프라인을 해체하자.– 예) 기존: ‘공격중’ 스테이트를 많은 AI종류가 공유대안: 각 AI 스크립트에 ‘공격중’ 스테이트를 둔다• 중복이 있더라도 부담 없도록, 표현이 간결해야 한다.• 비슷한 유형은 묶어서 표현할 수 있도록 하자.
  27. 27. 선택지• Lua• 다른 게임 스크립팅 언어들• 자체 제작
  28. 28. 선택지: Lua• 장점:– 널리 알려져 있다.– 나도 익숙하다.• 단점:– FSM을 원하는 만큼 간결하게 표현 못한다.– 단순 실수를 컴파일 타임에 잡지 못한다!NDC’11 <온라인 게임 처음부터 끝까지 동적언어로 만들기> 참고.
  29. 29. 선택지: 다른게임스크립팅언어들• AngelScript, Squirrel, GameMonkey, …• 잘 몰라서 탈락.어떤 특성이 있는지, 어떤 함정이 있는지…
  30. 30. 선택지: 자체제작• 할 수 있을까?• 정말 좋을까?• 비용은 얼마나 들까?
  31. 31. 할수있을까?Lua 5.0 구현 논문과, Lua로 된 Lua 파서 코드를 봤었다.딱히 외계인 기술이 아니다, 도전해 볼만한 일이라고 판단.
  32. 32. 정말좋을까?• 전에 해 봤다: FSM 언어– 카바티나 스토리– 캐릭터 조작계와 AI에 사용– 간단한 구조변수와 Expression 없음, 한정된 형태의 제어문– 적은 투자로 큰 효과를 봤던 기억[시작]전투중이면 -> 가만서있는다=0.5, [전투중]!다돌아왔으면 -> 제자리로돌아간다, [돌아가기]무조건 -> 배회한다=$배회주기[전투중]!전투중이면 -> 제자리로돌아간다, [돌아가기]스킬사용해본다( $스킬_평타 ) -> 시전성공무조건 -> 가만서있는다=0.1[돌아가기]전투중이면 -> 가만서있는다=0.5, [전투중]다돌아왔으면 -> 가만서있는다=0.5, [시작]무조건 -> 제자리로돌아간다
  33. 33. 비용은얼마나들까?• 개인 시간에 proof-of-concept을 시도해 보았다.• if문과 변수 선언을 갖는 언어를 구현하는 데 며칠 걸림.할 수 있겠다!
  34. 34. HFSMHierarchicalFiniteStateMachine스테이트 머신을 위한 문법실행 중 일시 정지한글 식별자멤버 변수 초기화구문 강조
  35. 35. HFSM 언어: 잘 한 것주로 언어의 모양과 기능
  36. 36. 스테이트머신을표현하는문법• [State]• # Update { … }• # Enter { … }, # Leave { … } 실제로는 거의 사용하지 않았다.• if (…) { goto [State]; }
  37. 37. 실행중일시정지• yield;• 홀드(30)~;• 공격준비동작실행(…)~;(일종의) 코루틴.함수호출이 없으므로 중간 상태 보관 구현이 아주 간단했다.잘 쓰면 스테이트 개수를 아주 많이 줄일 수 있다.
  38. 38. 컴파일타임타입체크• 모든 문Statement의 유효성과 식Expression의 타입을컴파일 타임에 체크한다.– 컴파일 타임 = 스크립트 로드 시점– 바인딩 정보를 활용한다.일단 로딩에 성공하면 스크립트 실행중 에러가 나지 않는다.격렬한 리팩토링도 무섭지 않다.
  39. 39. 사용자정의타입• 클래스의 바인딩을 C++ 코드로 작성한다.• 스크립트에서 바로 멤버 변수를 읽고 쓸 수 있다.• 스크립트에서 바로 멤버 함수를 호출할 수 있다.스크립트에서 직접 클래스를 선언하는 문법을 만들지 않았다.
  40. 40. 멤버변수초기화var 탈것서브어택 = AttackType() {준비시간 = MinMax(40, 45),공격액션 = "SubAttack"};var 탈것서브어택 = AttackType();탈것서브어택.준비시간 = MinMax(40, 45);탈것서브어택.공격액션 = "SubAttack";• C#을 모방.• 복잡한 데이터 서술에 좋다.• 일반적인 데이터 로딩에도활용할 수 있을 것 같다.=
  41. 41. 파생스크립트• 비슷한 AI들이 많다.• 다 따로 만들면 관리하기 어렵다.아직 코드가 굳어지기 전에는 더욱.• 상속 같은 걸 끼얹나?
  42. 42. 파생스크립트• 스탠드얼론 AI 스크립트– 모든 기능을 사용할 수 있다.– 행동의 절차를 서술한다.• 확장 스크립트– extend XXXX; 로 시작한다.– Init 전역 함수만 만들 수 있다.– 수정이 필요한 설정이나 전역변수를Init에서 변경한다.
  43. 43. 파생스크립트“예전 구조랑 똑같지 않나요?”• 한 AI = 한 파일이기 때문에 Copy&Paste가 부담 없다.• 스탠드얼론 AI 스크립트를 여러 종 둘 수 있다.• 특이한 구현이 일반적인 구현을 더럽히지 않는다.• 특히 보스 AI의 구현이 완전히 격리된다.
  44. 44. 매뉴얼자동생성• 바인딩 정보는 이미 다 가지고 있다.• 어떤 함수와 클래스가 있는지 html 파일로 출력하게 했다.
  45. 45. 실행로그• 그 틱에서 실행한 라인을 표시한다.• 브레이크포인트보다 훨씬 편하다.• 특히 여러 AI를 동시에 관찰할 때.
  46. 46. 한글식별자• 상당수의 함수에 주석이 필요 없었다.
  47. 47. 에디트플러스구문강조파일• 만들기 쉽다.
  48. 48. HFSM 언어: 아쉬운 것주로 내부 구현
  49. 49. 파서/타입체커/코드생성기가한몸• 1-pass• Recursive Descent 파서의 재귀호출 과정에서 모든 작업을 수행• 별 생각 없이 루아 컴파일러 구조를 모방한 것;;;문법 바꾸거나 새로운 거 추가할 때 고생했다.파스 트리를 먼저 생성하고 나서,그 파스 트리를 해석해서 타입체크하고 바이트코드를 생성하는 것이 바람직할 듯.
  50. 50. 정수형이없다• 값으로 취급하는 내장 타입들: number, bool, string• HFSM number = C++ float• 이것도 별 생각 없이 루아 모방…C++ 코드에서 매번 캐스팅하기 은근히 귀찮다.
  51. 51. 사용자정의타입은힙에만넣을수있다• 스크립트에 바인딩하는 클래스는 ReferenceCounter 상속을 강제했다.• 별 생각 없이 자바 모방아주 간단한 객체에도 스마트 포인터를 써야 하는 부담이 있다.
  52. 52. 레지스터머신• number/bool, string, object의 레지스터 3종 세트• 전역변수가 점유하고 남는 레지스터 공간을 지역변수가 사용스크립트 언어 안에서의 함수호출을 만들 수가 없네?;레지스터 공간 추적하기도 엄청 까다롭네?;스테이트 사이사이에 전역변수를 추가할 수가 없네?;1-pass 컴파일러다 보니…
  53. 53. 제네릭리스트구현• ‘객체의 리스트’ 문법을 내장.• 리스트 구현은 한 벌만 짜고, 컴파일러 트릭으로 구현을 공유했다.• 문법은 파이썬, 구현은 자바를 모방 역시 별 생각이 없었다.귀찮은 문제가 잡다하게 발생;;타입 체크가 아주 복잡하다던가리스트를 객체의 멤버로 넣을 수가 없다던가내장 타입의 리스트를 만들 수가 없다던가 (더러운 박싱 언박싱…)var 일반공격1 = AttackType() { … };var 일반공격2 = AttackType() { … };var 일반공격목록 = [ 일반공격1, 일반공격2 ];
  54. 54. 바인딩• 템플릿+매크로 서커스역시 별 생각이 없었다. 익숙한 대로 했을 뿐…• 읽기도 고치기도 어렵다.• 노출한 함수를 C++에서 호출하기 어렵다.특수한 주석으로 추가하고, 코드생성 하는 게 낫지 않을까?(NDC’11 코드 생성을 사용해 개발 속도 높이기 / 김이선님 발표)
  55. 55. 다시만든다면• 스마트 포인터를 언어의 핵심에서 제거하고,• 모든 타입을 값으로 취급한다. The STL Way!• 스크립트 스택 메모리에 직접 객체를 할당하고 관리.• 제네릭도 제거한다. List<A>와 List<B>는 전혀 다른 타입으로 취급.• 레퍼런스 시맨틱을 쓰고 싶으면 스마트 포인터를 직접 바인딩하게 한다.C++와 심리스하게 붙는 스태틱 타입 스크립트 언어를 만들려면, 이게 가장 심플한 답이 아닐까…
  56. 56. 요약 & 결론
  57. 57. 코드핫로딩• 실행 중 DLL 교체!• 간단히 적용 가능.DLL 빌드 대응이 되어 있지 않으면 좀 힘들겠지만…
  58. 58. 자체제작스크립트언어• 잘 쓰고 있다.• 의외로 만들기 어렵지 않았다.• 더 넓은 범위에서 써도 되지 않을까!• 내부 구현이 좀 아쉽다.생각없이 베끼다 보니까…
  59. 59. 게임 스크립트 언어, 만족하십니까?
  60. 60. Q/A
  61. 61. 명령어구조• 컴파일하면 코드 뭉치와 각종 테이블이 나온다. 컴파일을 먼저 해서 배포하는 것을 염두에 둠.• 1 명령어 = 32비트. 19종류. 레지스터는 9비트.• 명령어의 맥락을 보고 3종류의 레지스터 중 어느 것을 쓸지 알 수 있다.모르는 경우에는 명렁어 내부에 힌트를 인코딩해 넣어서 해결.• 함수호출이나 필드 액세스의 경우 별도의 테이블이 있고 명령어에는 테이블 인덱스만 저장.– 예) 함수호출: this 레지스터, 인자가 담긴 레지스터들, 리턴값을 담을 레지스터, 호출 직후에 yield할까• 스트링/숫자 리터럴도 별도의 테이블에 넣고 명령어에는 테이블 인덱스만 저장.

×