사람마다 회사마다 다르겠지만 제가 면접관으로서 아티스트를 살펴볼 때 중요하게 생각하는 내용들을 적어보겠습니다.상당히 오랜 기간 게임회사 면접관 역할을 하면서 느낀 것이니 입사 지원자나 회사 모두에게 참고가 될 것으로 생각됩니다. 그리고 여기에 언급되지 않았어도 중요한 것들이 무척 많습니다. ^^
< 포트폴리오가 가장 중요하다 >
자기소개, 포부, 스펙 등 지원자가 아무리 열심히 작성한 글이라도 아티스트를 뽑을 때에는 포트폴리오를 가장 먼저 봅니다. 애니메이터도 마찬가지구요. 정확히 통계를 내진 않았으나 열명 중 일곱 명 정도가 포트폴리오에서 탈락되고 나머지 세 명 정도만 경력이나 자기소개를 살펴봅니다. 그리고 그중 한 명 정도 면접을 볼지 말지 고민하게 됩니다.
포트폴리오의 구성도 중요한데요, 입사지원자 입장에서는 얼마나 의미있는 작품인지는 모르겠으나 객관적으로 수준이 떨어지는 작품이 섞여있으면 안좋은 포트폴리오입니다. 입사 후 안좋은 수준의 결과물을 보여주는 경우가 압도적으로 많거든요. 이유는 여러가지겠지만 경력이 있는 입사지원자라면 전 직장에서 선배의 지도에 의해서 실력보다 좋은 포트폴리오를 만들었을 수도 있고 팀 작업에 의해 본인의 실력보다 더 좋아보이는 포트폴리오일 수도 있습니다. 자기 작품을 객관적으로 평가하는건 어렵지만, 수준이 떨어지는 포트폴리오를 포함시켰다는 것은 그만큼 그 사람의 안목이 낮다는 뜻입니다. 아티스트는 자신의 눈높이만큼 보고 자신의 눈높이만큼 해내는 종족입니다. 포트폴리오를 구성할 때 주변 사람들에게 의견을 물어보고 객관적인 관점, 색다른 관점으로 자신의 포트폴리오를 다시 바라보는 것도 좋은 방법이라고 생각합니다.
포트폴리오의 퀄리티는 매우 우수하지만 프로젝트에서 원하는 스타일이 아닌 경우도 있습니다. 예를 들어서 현대/ 근미래 세계관을 만들어야 하는 프로젝트인데 입사지원자의 포트폴리오는 중세 환타지 세계관이 대부분이라던가 하는 등의 상황입니다. 이때 '이 정도 실력이면 현대물쯤이야 쉽게 할거야' 라고 낙관하는 것은 매우 위험합니다. 입사지원자가 환타지 세계관의 작업을 주로 했던 이유가 분명히 있을 것이며 혹시 어쩔 수 없이 지원자의 취향과 다르게 환타지 세계관을 그렸다 하더라도 현대/근미래 세계관에 익숙해지고 공부해야 하는 비용이 발생하게 됩니다. 그리고 '현대물 쯤이야' 라고 생각한 것 부터가 오산입니다. 어떤 세계관이든 잘하려면 정말 어렵습니다. 정 환타지 실력이 아깝다면 현대/근미래 주제로 과제물을 내는 것도 좋은 방법입니다(다만 이럴 때에는 과제비를 지급하는 것이 바람직합니다). 어쨋든 저쨋든 프로젝트 방향성과 맞지 않는 사람들이 섞여있게 되면 서로 피곤해집니다. 1+1 = 0.5 가 되는거죠.
< 성장 가능성 >
포트폴리오의 실력이나 스타일은 적당하지만 그에 비해 경력이 많다면 성장 가능성을 의심하게 됩니다. 반대로 경력에 비해 실력이 좋거나 성격이 긍정적이고 적극적이고 성실하다는 판단이 들면 앞으로가 기대되는 사람이라고 생각됩니다. 하지만 성장 가능성의 요소는 딱 꼬집어서 말할 수 없는 애매한 부분이네요.
< 커뮤니케이션 >
아티스트에만 해당되는 것은 아니겠지만 면접관의 질문을 정확하게 이해하는지, 답변할 때 장황하지 않게 핵심을 잘 말하는지 등이 중요합니다. 지원자의 말하기와 듣기 중 어느게 더 중요하냐라고 한다면 듣기가 더 중요하다고 생각합니다. 물론 개인적인 기준이구요. 가만히 있으면 중간이라도 간다는 말이 있죠. 이게 커뮤니케이션에서 중요합니다. 어설프게 자꾸 끼어들지 말고 잘 경청하는게 중요합니다. 물론 면접 자리에서는 입사지원자의 말이 많을 수밖에 없긴 하지만 최대한 면접관의 말에 집중해야 합니다.
< 전 회사 평판, 퇴사 사유 >
바닥이 좁아서 조사하면 바로 나옵니다. 거짓말 하면 안됩니다. 술자리에서 업계 얘기들 들어보면 쓰레기들 참 많습니다. 하지만 평판이라는 것이 정치가 심한 조직일 수록 상대적인지라 참고 정도로 해야합니다.
< 게임 개발이 좋은건지 아트가 좋은건지 >
게임과 게임 개발이 좋아서 아티스트를 하는 경우도 있고, 반대로 아트가 좋아서 게임 개발을 하는 경우도 있습니다. 어떤 사람은 반반 섞이기도 하고 어떤 사람은 한 쪽으로 많이 치우치기도 하고 사람마다 가지각색입니다. 어떤 것이 좋다고 볼 수는 없지만 어떤 사람인가 판단할 때 중요하게 보는 요소입니다.
예를 들어 프로젝트 규모가 작고 팀원들의 자발적인 아이디어 참여를 독려하는 분위기의 조직이라면 아티스트보다는 개발자 성향의 사람이 더 좋은 선택이 될 것입니다. 반대로 프로젝트 규모가 크고 체계적인 관리 시스템에서라면 아티스트 성향의 작업자가 더 좋은 선택이 될 것입니다. 물론 어느 선택이더라도 아티스트가 최소한의 퀄리티는 내준다는 전제가 붙긴 합니다.
개인적으로는 아티스트보다 개발자가 더 좋더군요.
이런 관점에서 주로 물어보는 질문들은 게임은 좋아하는지, 어떤 게임을 좋아하는지, 콘솔은 어떤걸 가지고 있는지, 만들고싶은 게임은 뭔지 등입니다.
< 기술 성향과 아트 성향 >
아티스트들 중에서도 툴의 고급 기능을 활용하는 사람이 있고 기본 기능만으로 작업하는 사람도 있습니다. 아티스트의 감각과 눈높이가 중요하고 툴은 도구에 불과하기 때문에 얼마나 툴을 잘 쓰는가는 크게 중요하지 않습니다. 하지만 어떤 성향의 사람인지 판단하는 중요 요소이고 어떤 역할을 맏길지의 기준이 되기도 합니다. 경험상 기술 성향과 아트 성향이 두루 섞여있을 때 서로에게 도움이 되는 경우가 많았습니다.
< 목표 >
목표가 AD인 경우도 있고 사장인 경우도 있고 세계 최고의 애니메이터인 경우도 있습니다. 이것은 입사지원자가 합격해서 같이 일하게 되었을 때 어떤 행동을 할 것인지 예측할 수 있는 요소이고 프로젝트에서 어떤 역할을 맏기면 좋을지 판단할 수 있는 기준이 됩니다. 보통 가까운 목표와 먼 목표 두 가지를 물어봅니다.
끝.
PS. 포스팅이 하루 늦었네요. 이게 다 티리엘때문입니다. 그나저나 점점 밑천이 떨어져서 큰일입니다... =_=;;
이처럼 보편적인 내용을 제가 또 다루어보는 이유는 저도 만들어보면서 참 어려웠기 때문입니다. 이 기술을 적용했을 때 뭔가 이렇게 환상적으로 좋아지는 결과를 생각하지만, 실제로 구현을 해보면 의외로 원하는 결과를 얻는 것이 생각보다 쉽지 않았습니다.
그래서 오늘은 Bloom 효과가 어떤 것인지에 대해서 알아보고, 게임에서 Bloom을 어떻게 하면 좋게 만들 수 있을지에 대해서 제가 고민하면서 이런 저런 자료를 통해서 알게된 내용을 한번 정리해서 이야기 해볼까 합니다. 그렇다고 제가 이렇게 하면 완벽한 Bloom을 만들 수 있어요! 라고 이야기하는 건 아닙니다. 그걸 알면 저에게 알려주세요~. ^^
(분량 관계로 자세한 설명에 대해서는 관련 자료를 링크해서 설명하도록 하겠습니다.)
그렇다면 Bloom 효과란 무엇일까요?
카메라 렌즈에 빛이 들어올 때 카메라가 100% 정확히 그 빛의 정보를 담아내지 못하기 때문에, 태양과 같이 강렬한 빛을 찍으면 실제로 보이는 빛보다 더 큰 면적으로 찍히게 됩니다. 이런 현상은 마치 빛이 퍼져보이는 것과 같은 효과를 나타냅니다. 이것을 흉내내서 처리하는 것을 우리는 Bloom 효과라고 합니다.
Bloom 효과의 정의는 다음과 같습니다. 보신 바와 같이 "강한 빛을 찍으면 실제로 보이는 빛보다 더 큰 몇적으로 찍히게 됩니다" 가 가장 큰 포인트입니다. 따라서, 위에서 언급한 Bloom의 성질에 의하면, 사실 위에서 처럼 강제로 뭉개서 뽀샤시 하게 만든 것은 실제로 Bloom의 성질과는 맞지 않습니다 (물론, 설정에 따라서 만들어 내지 못하는 것은 아닙니다만...). 즉, Bloom 효과와 뽀샤시 효과를 같은 것이 아니라는 이야기입니다. 우리가 만들려고 하는 것은 강한 빛을 받는 부분이 번지는 것이지 화면이 전체적으로 뽀샤시해지는 것이 아니라는 것을 정확히 해야 합니다!
그렇다면 게임에서 Bloom을 만들려면 어떻게 해야 할까요?
가장 먼저 Bloom하면 빼놓을 수 없는 것이 HDR(High Dynamic Range)이기 때문에 HDR을 이야기 하지 않을수가 없습니다. 하지만, HDR이란 주제가 너무 방대하기 때문에, 여기서는 제가 이전에 발표했던 자료로 대체하도록 하겠습니다. (알콜코더님의 발표자료도 함께 보시면 좋아요: http://gamedevforever.com/168)
게임 그래픽 특히 포스트 프로세싱이 본격적으로 사용되고 있는 현 시점에서 HDR은 굉장히 큰 의미를 가지고 있습니다.
Bloom 이라는 효과의 기본 또한 HDR입니다 입니다. 물론 HDR을 사용하지 않다고 하더라도 Bloom 효과 자체는 표현이 가능합니다. 그래서, 우리는 구현 방식에 따라서, HDR Bloom과 LDR Bloom (유사 HDR)로 구분해서 사용을 하기도 합니다. 모든 Bloom 파이프라인을 Float Buffer에서 처리하는 방식을 HDR Bloom이라고 하고, HDR 장면을 LDR로 변경시킨 후 Bloom 파이프라인을 처리하는 것을 LDR Bloom 이라고 합니다.
[HDR Bloom 과 LDR Bloom "HDR In Valve's Source Engine" Siggraph 2006]
그럼 간단하게 Bloom 효과를 만들어 내는 방법을 알아보도록 하겠습니다.
렌더타겟에 장면을 렌더링 한다.
렌더타겟에 렌더링된 장면에서 밝은 부분을 추출해서 렌더링한다.
추출해낸 밝은 부분을 가진 렌더타겟을 블러시킨다.
렌더링된 장면 렌더타겟과 블러된 렌더타겟과 합성한다.
굉장히 간단하죠?! 사실 이게 전부입니다. 하지만, 이렇게 간단한데 이상하게 구현을 해보면 원했던 결과를 얻기가 쉽지 않았습니다. 그래서 과연 무엇을 놓치고 있는가? 어떻게 하면 좀 도 좋아 보이게 만들 수 있을까? 아니, 다른 게임들처럼 좋은 Bloom 효과를 만들 수 있을까? 에 대한 고민을 하게 되었습니다.
그럼 여기서 좀 더 나은 Bloom을 만들기 위한 방법들을 한번 알아보도록 하겠습니다.
* 밝은 부분을 추출하자!
블룸 현상은 강렬한 빛이 찍혔을 때 나오는 현상이기 때문에 실제로 렌더링된 장면에서도 이런 특성에 맞추어서 밝은 영역을 잘 추출해내는 것 가장 중요한 문제입니다. 하지만, Bloom을 처리할 때 부딪히는 문제 중에 하나가 바로 여기에 있습니다.
실제 화상에서는 어지간히 강한 빛이 아니면 큰 번짐 효과가 나오지 않습니다. 이와 마찬가지로 HDR 렌더링이라고 하더라도 장면의 밝은 영역의 대역이 너무 낮은 영역에 포진되어 있다고 하면, 원하는 밝은 부분을 추출하기가 쉽지 않습니다. 그런 이유로 위와 같은 디퓨즈 영역까지 블룸이 적용되는 원하지 않는 결과가 발생하게 됩니다. 물론, 의도적으로 이렇게 뽀샤시한 느낌을 주기 위해서 낮은 영역을 포함하기도 합니다.
[밝은 영역 추출하기]
이처럼 밝고 어두운 부분의 영역이 충분하지 않다면, 전체적으로 어두어야 할 부분도 번져버리는 결과가 나타나게 됩니다. 이렇게 올바르지 않은 밝은 영역을 찾는 것을 줄이기 위해서는 방법은 무엇이 있을까요?
- Bloom Curve
물리적으로 올바른 방법은 아니지만, Bloom에 의해 불필요하기 밝아지게 되는 부분을 줄이기 위해서 Bloom Curve를 적용하여 밝은 부분과 어두운 부분의 영역을 보장시킨다. 이는 Bloom을 적용하기 전에 HDR buffer에 curve를 적용해준다.
[Siggraph2010 "Physically Motivated Shading Models for Game Development"]
* 좀 더 높은 퀄리티의 블러 만들기
우리는 Bloom 효과를 만들기 위해서, Bloom 이미지를 부드럽게 블러시키기 위해서 Gaussian Blur를 사용합니다. 하지만, Gaussian Blur는 커다란 블러 반경에다가 부드럽게 블러를 먹이려고 한다면, 부하가 높아서 리얼타임으로 사용하기가 어렵습니다. 그렇기 때문에 보통 Bloom 이미지를 다운스케일해서 블러시키면 좀 더 넓은 영역을 블러 시킬 수 있기 때문에 많이 사용하지만, 이럴 경우 퀄리티가 많이 훼손되게 됩니다. 이런 퀄리티를 높이기 위해서는 어떻게 해야 할까요?
- Multiple Gaussian Filter
Masaki Kawase 씨가 발표하신 내용으로 좀 더 저렴한 비용으로 높을 퀄리티를 내기 위해서 소개한 방법입니다. 자세한 설명은 이 페이지를 참고하시면 좋을 듯 합니다(http://allosha.tistory.com/50).
[GDC2004 "Practical Implementation of High Dynamic Range Rendering"]
이 기법은 이후 다른 게임들에도 많이 사용되었는데, Halo3의 구현 사례를 보도록 하겠습니다.
Halo의 경우도, 유사하게 단계적으로 블룸 이미지를 다운스케일하여 그 이미지들을 블러시켜서 합성해주는 방법을 사용합니다. 이렇게 하면 일반적으로 Bloom 샘플에서 보여지는 한장의 다운스케일 이미지를 이용해서 Bloom을 처리하는 방식보다 높은 퀄리티로 넓은 범위를 블러시키는데 도움을 줍니다.
- Flickering Artifact
저렴한 비용으로 넓은 범위를 블러시키기 위해서 다운스케일을 많이시키다보면 가우시안 블러를 적용해야 할 블룸 이미지의 픽셀 수가 부족하게 되기 때문에 카메라를 이동함에 따라서, 부족한 픽셀 때문에 블러가 적용되는 범위가 굉장히 영향을 많이 받게 됩니다. 이런 현상은 결과적으로 블러되는 부분이 깜빡 깜빡하게 되는 문제가 발생하게 됩니다.
이를 해결하기 위해서는 다운스케일 할 때, 좀 더 넓은 범위의 픽셀들의 평균으로 내 픽셀의 값을 만들어내면 이렇게 이동에 따른 깜빡거리는 현상을 조금을 줄일 수 있게 됩니다. 하지만, 그렇다고 하더라도 기본적으로는 너무 많은 다운스케일을 하여 픽셀 수가 너무 많이 부족하게 되면 문제가 발생할 수 있기 때문에, 적절히 가우시안 블러의 범위와 다운스케일 사이즈를 결정하는 것이 중요합니다.
* 결론
실제로 Bloom 효과가 렌더링 퀄리티에 미치는 영향은 꽤 큽니다. 그러니, HDR + Bloom의 퀄리티는 전체 화면의 품질을 결정짓는 가장 기본이라고 볼 수 있을 것입니다. 그런 이유로, 지금까지 Bloom 효과를 만들어 낼 때에 고민해봐야 할 것을을 한번 다루어 보았습니다. 우리가 많이 사용하고 있는 Bloom 효과이지만, 의외로 생각할게 많지요?
저도 Bloom을 놓고 고민했을 때, 의외로 정리된 자료가 없고, 원하는 퀄리티가 나오지 않아서, 참 괴로워했습니다(물론, 아직도 원하는 퀄리티를 만들어내지는 못했습니다.. ㅜㅜ). 그래서, 제가 나름 공부하면서 이런 저런 자료들을 보고 적용해본 내용들을 한번 정리해 보았습니다.
어쩌면 저 같이 Bloom 효과에 대해서 많은 고민을 하셨던 분들이 있다면 그 분들에게 제가 고민했던 이런 내용들이 조금이나마 도움이 되었으면 좋겠습니다. ㅎㅎ. (혹시 좋은 결과가 나오시면, 저한테도 그 사례를 공유해주시면 감사하겠습니다. ㅎㅎ)
3차 클베의 가장 큰 특징 중 하나는 바로 '활력 시스템' 의 도입입니다. 안해보신 분들 혹은 관심없는 분들을 위해 말씀드리면 이 시스템은 2차까지는 없었던 시스템으로, 던파나 드네의 그것과 비슷한 일종의 피로도 시스템 같은 것입니다. 와우로 치자면 휴식 경험치 같은 것인데요, 이번 3차 클베의 가장 큰 이슈가 되었습니다. 안좋은쪽으로요.
허벅지가 튼튼한게 큰 이슈인 우리 막내
활력 시스템의 구성은 이렇게 되어 있습니다.
활력 시스템의 기본과 상태 -
- 활력 상태는 활력 / 일반 / 피로 세 가지 상태로 분류됩니다.
- 활력 상태는 파랑 / 일반 상태는 초록 / 피로 상태는 회색으로 표시됩니다.
- 활력 상태에 따라 경험치를 각각 220% / 150% / 10% 로 적용 받습니다. (패치 전에는 200% / 100% / 10%)
그래서 광렙을 하게 되면 활력을 점점 소모하게 되어, 더 이상 경험치를 쌓을 수 없는 지경에 이르게 됩니다. 여기서 유저들은 첫번째로 '정기사용요금을 전제로 하는 게임이 사용자의 진행을 막는다는 것은 있을 수 없는 일' 이라며 크게 반발했었습니다.
이정도는 한 눈에 보면 알 수 있는 것들이고, 그 외의 사항에 대해서는 개발진에 의해서 자세하게 밝혀진 것들이 없었습니다. 그래서 게임을 해보면서 몇가지 연구를 조금 해봤습니다.
1. 활력을 통해 얻는 경험치의 증가량은 사냥과 퀘스트 등 모든 경험치에 적용된다.
- 말 그대로 활력 상태별로 얻는 경험치 보너스는 모든 경험치에 적용됩니다. 계속 언급 되겠지만, 월드 오브 워크래프트의 휴식 경험치는 사냥 경험치에만 휴식 경험치 보너스가 붙습니다.
2. 활력의 소모는 경험치를 얻음으로서 소모된다.
- 활력은 시간 경과나 그 밖의 행동으로는 소모되지 않으며, 오직 경험치를 얻는 행위 (사냥, 퀘스트 완료 등)을 통해서만 소모됩니다.
3. 활력의 회복은 마을등 특정 활력 회복 구간 혹은 로그아웃을 하는 것으로 회복된다.
- 활력이 회복되는 곳에서 채팅이나 PVP, 제작 등의 일을 하면 활력을 회복시킬 수 있고, 로그아웃 하는 도중에도 회복이 가능합니다. 월드 오브 워크래프트에서는 마을의 여관에 가야지만 회복이 가능합니다.
4. 활력을 회복했다면 그 양 만큼의 경험치 보너스를 보장한다.
- 활력을 소모해 간다고 해서 최초 보장했던 양이 점점 줄어든다거나 하지 않습니다.
이정도 놓고 보면 뭔가 그렇게 큰 문제가 있어보이지는 않습니다. 그렇다면 도대체 무엇 때문에 활력 시스템이 이렇게 화제가 되었을까요? 그래서 캐릭을 다시 처음부터 키우면서 하나씩 체크를 해봤습니다.
먼저 최초 프롤로그를 마치고 나면 그때부터 활력 시스템이 시작됩니다.
위에서 보신 사진입니다만... 일단 처음 활력 시스템이 적용되면 레벨 4에서 레벨 11의 0% 그러니까 레벨 10까지 올릴 때 까지는 무조건 활력 220% 경험치를 보장받게 됩니다. 여기서 한가지 더 알 수 있는 활력 시스템의 법칙은
5. 활력은 최대치까지 차오르면 그 이상은 올라가지 않는다.
- 일단 활력 상태가 되면 그 활력을 일정량 소비하기 전 까지는 더 이상 활력이 회복되지 않습니다. 그러니까 활력이 최대치까지 차게 되면 더 이상 활력을 회복하는 일이 무의미하다는 말입니다. WOW 에서는 휴식 8시간에 경험치칸1칸씩 휴식 보너스를 받게 되며, 8칸이 최대입니다. (64시간)
여기서 유저들은 '정기 요금을 결제해도 게임을 자주 못하거나 주말에 겨우 접속하는 라이트 유저라면 활력을 다 쓰지도 못해서 마찬가지로 손해를 볼 것이다'라는 시나리오를 가정하고 역시 반발했습니다.
처음에는 '그래도 레벨 4에 11까지는 활력 220%를 보장해주니 꽤 많지 않은가?' 하고 생각을 했는데, 220%로 바뀌기 전 200%일때는 레벨 16까지 활력 상태가 보장 되었다고 하더군요. (이건 사실 확인을 해보지 못했습니다)
그래서 경험치 테이블을 작성해 보기로 했습니다.
최초 활력을 보장받는 구간의 경험치를 기록해봤습니다. 레벨 4부터 10까지 필요한 경험치의 총량은 35,600 입니다. 그러면 대충 '오 35600 씩이나 경험치 활력 구간을 보장받으면 다음 활력 상태일때도 그정도 양을 보장받겠군' 이라는 유토피아를 꿈꿨습니다. 그리고 초록 상태에서 가끔 게임을 접속하며 시간당 활력이 얼마나 회복되는지 지켜보기로 했습니다.
초록색 일반 상태의 활력을 거의 소모하지 않은 상태에서 약 3일을 들여서 활력이 차오르는걸 지켜봤는데 결과는 조금 참담했습니다. 무엇보다 레벨 4-10 까지 활력 보너스를 보장하던 구간이, 11에 오르자마자 13의 0%, 그러니까 약 2레벨이 조금 안되는 구간만 보장을 하는 것으로 태도를 싹 바꾸는 통에 멘탈 붕괴될 뻔 했습니다. 레벨 11과 12는 각각 필요 경험치가 7200, 7800 밖에 되지 않습니다. 최초 35600의 활력 보너스를 제공하다가 갑자기 절반 정도로 뚝 떨어져버리게 된 샘입니다.
레벨 27 캐릭터는 말할 필요도 없지요. 활력이 완충되기까지는 24시간 이상이 필요했습니다. 저렇게 10레벨대 초반에나 약 2레벨 정도의 활력 상태 보너스를 제공하고 이후에는 1레벨 정도로 빠듯하게 주어지게 됩니다.
여기서 한가지 더 함정이 있습니다.
6. 활력 상태는 일정량을 소비하지 않으면 회복되지 않는다.
- 정확하게 얼만큼인지는 확인해보지 못했습니다만, 활력 상태에서 일정량의 활력을 사용하지 않는다면 활력이 회복되는 일이 없습니다. 적어도 50% 이상은 소모해야 하는 것 같았습니다. 그렇지 않을 경우 플레이어의 활력 최대치는 항상 제자리에 있게 되며 활력 회복 지역이나 로그아웃 같은 행동이 무의미해집니다. 활력 회복 지역에 들어서면 '이 구역에서는 활력이 회복됩니다' 라고 나오는데 모두 뻥이 되는 샘이지요. 그래서 플레이어의 캐릭터는 더 이상 성장을 할 수 없게 됩니다.
7. 활력은 항상 정확하게 계산된다.
- 드네 게임에서는 피로도가 일단 남아만 있다면, 요구하는 피로도가 얼마든 상관없이 스테이지에 입장 가능합니다. 그러니까 10밖에 없어도 200짜리 스테이지에 입장하는게 가능합니다. 근데 이 활력은 다음 활력 구간까지 10 남았는데 내가 지금 받는 경험치가 100이라면, 얄짤없이 남은 양은 다음 활력 구간에 맞게 결정됩니다. 만약 피로 구간에 걸쳐있다면 경험치를 10%만 챙겨가는 사태도 벌어집니다. (그럼에도 불구하고 UI 적으로는 그냥 현재 구간에 맞게 출력되어, 그렇게 손해볼 수 있다는 사실을 모르게 됩니다)
문제점은 바로 피로 구간입니다. 경험치를 10%만 챙겨가게 되는데, 사실상 얻는 경험치가 1 정도에 불과해서 더이상의 사냥이나 퀘스트 진행은 불가능하게 됩니다. 무엇보다 퀘스트 경험치까지 빼앗아 간다는 것은 실로 엄청난 패널티가 아닐 수 없습니다.
어디서 본 것인지 도저히 생각이 안나는데 (혹시 아시는 분은 첨언 부탁) 본래 WOW의 휴식경험치는 휴식 상태 100%, 보통 상태 50% 로 했더니 게임 플레이 시간이 줄어들길래 휴식 상태 200%, 일반 상태 100%로 바꿔서 긍정적인 보너스로 바꿨더니 잘 되었더라- 하는 포스트모템을 본 기억은 있는데 도저히 그 출처를 찾을 수가 없습니다 ㅠㅠ ( http://www.slideshare.net/epiphany/blizzard-design-processkor-for-web-v10 avici 님의 제보입니다 감사합니다) 근데 이런 휴식경험치 개념을 관련된 사람들이 몰랐을리가 없거든요. 블&소 PD님도 와우 즐겨하시는 걸로 알고있고요.
사실 색깔도 조금 그런데요, 저같았으면 활력 상태를 번쩍번쩍 광나게 만들어놓고 '고갱님은 지금 엄청난 혜택을 받고 계십니다~' 하는 기분을 느낄 수 있게끔 만들고, 일반 상태에서는 파란색으로 하면 좋지 않았을까 생각해봅니다. 일반 상태로 떨어지면서부터는 뭔가 조급해지고 박탈감까지 느껴지거든요. 백화점에 창문이나 시계가 없잖아요. 고갱님들 시간가는줄 모르게. 근데 일부러 활력을 회복하려고 로그아웃 하게 만든다는 것은 뭐랄까 좀...
정말 이해가 안가는건 '왜 이렇게까지 해야 했느냐' 하는 점입니다. 굳이 10%로 깎아 가면서까지 플레이를 막아야 했을 정도로 컨텐츠 소모가 두려웠는지 거기에 더해 '더 자세한 정보를 알고 싶으면 NC에 입사해라' 라는 GM의 운영 미숙에 이후 220% 150% 패치와 더불어 활력 시스템의 수많은 건의에도 불구하고 일언반구도 없었던 소통의 부재까지 여러모로 다 된 밥에 스스로 재를 뿌리는 상황을 연출하고 말았습니다. 이 게임이 2차 CBT때까지는 극찬을 받았고 심지어는 '모든 게임이 까이는 그 곳'에서도 쉴드를 받았던 게임이라는 것을 상기하면 정말로 안타깝기 그지 없습니다.
활력 시스템의 기획 의도는 무엇일까요? 제가 기획자가 아니니 추측을 해보자면
- 레벨링을 늦춰서 유저간 레벨 격차 축소
- 레벨링 이외의 컨텐츠 (제작, PVP, 아이템 파밍 등) 사용 유도
정도가 아닐까 생각 해봅니다. 애피타이징 2주, 3차 클베 2주로 어느정도 데이터도 얻었을테고 중간의 220% 150% 패치도 뭔가 의도가 있었겠지요. 그런데 어제 클베 종료와 동시에 사업 총괄을 맡고 계신 전무님이 활력 시스템은 삭제하고 고객님들 말씀도 경청하고 있다는 공지를 올림으로서 활력 시스템은 개선의 여지 없이 사라질 운명에 처하게 되었습니다.
그래서 결론 : 아무리 의도가 좋아도 고갱님고객님을 이기려고 해서는 안된다. 싸움을 걸어서는 안된다.
저를 포함한 대부분의 개발자들은 버그를 만듭니다. 인재들이 모였다는 MS의 IE 베타버전이 얼마나 엉망인지 아시죠? QA와 오류 보고 시스템 등을 통한 검수가 정식 버전을 안정성있게 만드는 것이지 MS가 만든 제품도 버그가 많습니다. 그 들이 극찬하는 IOS나 OSX요? 네 그렇게 신중한 애플도 버그를 만듭니다. 특히나 많은 버그가 개발 과정에서 더더욱 많이 튀어나오는데, 이들 대부분이 개발자의 습관에서 비롯됩니다.
흔
히 많이 하는 실수가 초기화 오류(변수값의 초기화 값이 없다거나, 변수가 초기화 되는 시점이 잘못된 경우)라던가, 경계값
오류(임계치에 가까운 값을 잘못 설정한 오류), 연속 시도시 잘못된 결과가 도출되는 오류 (예를들면 결제 버튼 여러번 누르면 돈은
여러번 감소하고 아이템은 지급안된다던가하는 오류), 버그 수정시 다른 버그를 만들어 내는 문제 등이 있습니다.
생각보다 같은 종류의 실수를 자주 하는 사람이 많은데, 모 QA팀에서는 버그 스타일에 따라 담당자를 배정하는 사례도 있었다고 할 정도죠. 물론 어쩌다가 한번 나오는 버그는 개발자의 실수로 모두가 좋게 넘어가지만 같은 종류의 버그가 여러번 나오고, 제대로 된 수정이 이루어지지 않는다면 여러가지 측면에서 문제가 생깁니다.
QA팀에 없는 경우에는 개발팀 테스트 후 바로 서비스가 될 테니 더 말 할것 없고, QA팀이 있는 경우라해도 개발 과정의 버그도 줄여야 합니다. 반드시 그렇게 해야 하는 근거는 다음과 같습니다.
1. QA팀에 신뢰를 심어주기 위해 - 물론 QA팀은 몇번 같은 종류의 버그가 생겼다면 꼼꼼히 챙겨봐주신다. 하지만, 버그가 급격히 줄어든 걸 보면 뿌듯하고 개발팀에 신뢰도가 향상하게 되어있다. 2. 우리의 버그를 검증해주는 QA팀 마저도 놓칠 수 있기에 - 특히나 대규모 패치일 경우 더더욱 그런데, 체크 리스트 목록을 많이 작성하다보면 평소보다 테스트하는 디테일이 떨어질 수 있다. 3. 안좋은 습관은 긴급한 코드 작성시 극도로 심해지기 때문에 - 이건 주로 내가 그런데, 급하게 코드를 작성하다보면 안좋은 습관 (여러가지 측면을 다 고려해야 하는데, 현재 증상만 보고 버그를 수정한다거나 하는 경우)이 나온다. 4. 멍청하단 소리를 듣지 않기 위해서 - 같은 실수 여러번하면 멍청하다는 소리를 듣게 되있다. 듣기 싫다면 고쳐라.
이런 문제는 정상적인 개발자라면 대부분 2~3년차 때는 고치게 되는 편입니다. (안타깝게 비정상적인 개발자가 꽤 많죠...ㅠ_ㅠ 누구라고는 얘기 안하겠습니다.)
여하튼 개인의 습관에서 나오는 문제를 고쳐야 되는 이유에 대해 이야기 해봤습니다. 자 이제, 팀 차원에서의 버그 재발을 막기 위한 노력에 대해서 예를 들어 이야기 해보겠습니다.
개발자가 어느정도 숙련자 위주로 이루어져있는 팀이 있습니다. S프로젝트라고 지칭해보죠.
S프로젝트는 3년의 개발을 거쳐 OBT 직전에 와있는 사내에서도 주목받는 팀입니다. 그런데 이 팀에 인원이 부족하네요. 자 우리 회사는 어느정도 건실하고, 주목받는 팀에서 본보기가 되기 위해 신입도 뽑아보자고 마음을 먹었습니다. 10여명 면접을 보고 그 중 가장 성실하고 센스 있어 보이는 C를 뽑았습니다.
OBT가 시작됐습니다. 예상보다 반응이 좋습니다. 너무 반응이 좋다보니 유저가 폭발적이네요. 로그인이 잘 안된다고 합니다. 하지만 우리가 누군가요? 베타랑들 답게 능숙하게 트러블을 10분만에 빠르게 해결하고 정상화 시켰습니다. 게시판에서도 호평 일색입니다. 안정적인 서비스, 재미, 그래픽 모두가 만족스럽다고 합니다. 딱히 문제도 없다보니 OBT중이지만 이 행복한 팀은 OBT기간임에도 칼퇴근도 합니다.
반응이 좋지만 조금씩 불안해집니다. 미리 만들어놓은 컨텐츠가 1달치뿐이니까요. 슬슬 컨텐츠를 더 만들어야 할 때 입니다. 베테랑 개발자 A가 이 시기에 집안일로 어쩔 수 없이 자리를 비우게 됩니다. 해야 될 일은 많은데 한사람이 비어 어쩔 수 없이 C에게 컨텐츠 개발을 시켜봅니다.
그런데 똘똘해보이고, 센스 있어보이던 C가 실수 연발입니다. 개발과정이 A보다 몇배 오래 걸림은 물론이고, QA에서 발생한 버그를 한번에 수정하지 못하고 계속 여러번 오가게 만들어 QA도 늦게 끝나 야근하는 날이 늘어납니다. 다른 개발자들은 이 신입에게 불만을 토로합니다. 좀 모자란거 아니냐, 잘못 뽑은거 같다, 나 신입땐 안그랬다 등등... 팀 분위기는 조금씩 안좋아지고, 야근이 잦아지다보니 서비스 퀄리티는 조금씩 떨어집니다. 아 뭔가 잘못 되어가는거 같습니다. 우울해집니다.
노파심에서 말씀드리자면 이 이야기는 절대 신입을 뽑지 말자는 의미가 아닙니다. 상황이 이렇게 된데에는 잘 흘러가는 것처럼 보이던 S프로젝트 팀에 잠재적 문제가 있었습니다.
과연 이 팀의 문제가 무엇이었을까요?
1. 개개인의 차이에 맞지 않는 일정 수립 - 베테랑 개발자 A와 신입 개발자 C의 개발 속도 차이를 고려하지 않고 일정이 수립. - 여기에 + @로 신입 개발자 C는 코드 분석 시간과 업무 방식에 대한 적응 기간도 필요한데 이마저 고려되지 않았음.
2. 개발 검수 과정이 없었음. - 너무나 숙련된 개발자들로 이루어진 팀이었다보니, 개발 과정에 대해 누구도 관심을 갖지 않음. 각자의 할일에만 포커스가 맞추어져 있었음. - 개발 검수가 없었다는 것은 코드 검수도 이루어 지지 않았다는 것이고, 신입 개발자 C가 작성한 코드가 지금껏 암묵적으로 지켜져오던 룰을 깼을 가능성도 있음. - 분명 개발 검수 과정이 제대로 이루어졌다라고 한다면 적절한 로그를 남기도록 작업했을 터인데, 이마저도 이루어지지 않음.
3. 디버깅이란 경험이 많은 역할을 하는데, 이에 대한 리딩이 없었음. - 디버깅은 정상 동작과 비정상 동작의 차이와 증상을 힌트로 원인을 찾아내는 과정이다. - 신입 개발자인 C가 디버깅에 익숙할리가 없는데 이에 대한 방치로 인해 업무가 지연되었고, 이는 C가 아닌 개발팀이 만든 문제다.
실제로 이 팀에 내재된 문제는 더 있을 겁니다. 이런 상황을 만들었다는 것 자체가, 개발자 개개인의 역량에 지나치게 의존한다는 증거라고 볼 수 있기 때문입니다.
게다가 업무에 대한 교류와 검토가 이루어지지 않다보니, 베테랑 개발자 A라 할지라도 순간 정줄을 놓는 순간 버그를 양산할 여지가 다분합니다. 문제는 버그가 양산된다해도 우리의 희망 QA팀에서 대부분 잡아주시겠지만 개발 규약이라던지, 로그가 적절하게 취합되고 있는지, 예외 상황은 얼마나 잘 처리되었는지 등에 대한 검토마저 개개인의 역량에 맡기고 있다는 사실입니다.
요
새 PC가 워낙에 빠르다지만 클라이언트는 유저에게 배포되기 때문에, 서버는 많은 유저를 수용하고 그들의 요청을 처리해야 되기
때문에 속도도 중요한데, 검수가 없다는 것은 이 코드가 정상 상황에서 잘 돌아가기만 하면 된다고 여기고 있다는 얘기가 되는
겁니다.
이렇듯 꽤나많은 문제를 갖고 있는 이 팀이 문제들을 해결하기 위해선 몇가지 노력이 필요합니다.
1. 오류 보고 시스템
- M2 프로젝트에서 개발과정 도입되었다고 알려져 있는 이 시스템은, 나는 5명이상의 프로그래머가 함께 일하는 모든 개발팀에서
도입되어야 한다고 생각한다. 오류 보고 시스템을 적극 사용하는 과정중에 코드 검수가 이루어지며, 런타임 오류가 발생한 위치와
작성자, 원인 등을 명확히 파악하기에 유용하기 때문이다.
2. 개발 규약 - 암묵적이 아닌 개발
규약은 어느정도 존재할 필요가 있다. 뭐 띄어쓰기는 어떻게 하고, 네이밍은 어떻게하고 이런거보다는, 예외 처리는 어떻게 해야
하는지, 로그 기록에 대한 기준, 외부 라이브러리 도입 기준, 코드 관리 규약 (만들어져 있는 기능들에 대한 명세 및 관리) 등에
대한 이야기를 정리해놓고 손쉽게 관리하도록 노력하는 것이좋다.
3. 코드 리뷰 - 코드 리뷰를
어렵게 생각하지 마라. 개발 전에 작업 방향에 대해 이야기를 나눈 이후, 개발 테스트 이전이나 개발 테스트중 코드 검수를 요청하면
된다. 작업 방향에 대한 이야기가 선행되어 있기 때문에 전면적 리팩토링을 하게 될일은 없을 터이고, 개발 테스트 도중 발견됐을
버그나, 부분적인 코드 완성도를 끌어올리고 개발 규약에 맞는지, 유저에게 오류 발생시 어떻게 보여지고 어떻게 처리될지에 대한
고려를 중점적으로 보면 된다. - 다만 지나치게 잦은 코드 리뷰와, 코드의 큰 방향성을 뒤흔드는 코드리뷰는 옳지 않다. 이는 개발 미팅 후 개발자들 간의 조율 과정에서 끝나야 한다.
4. 자동화 된 테스트 -
유닛테스트 + 모듈 테스트로 분리되어 얘기되기도 하는데, 분리 가능한 코드 조각을 테스트 하는 것이 유닛 테스트, 외부 프로그램
내지는 포함된 코드가 실제 동작중인 프로그램을 대상으로 반복된 테스트를 통해 새로 작성된 코드가 이미 작성된 기능을 망가뜨리진
않았는지, 또 새로 작성된 코드에 대한 테스트를 작성함으로써 해당 기능에 대한 이해도를 높이는 순작용도 가지고 있다. - 또한 자동화된 테스트를 통해 크래시가 얼마나 자주, 또 매번인지 때때로 발생하는 지 등도 알아 낼 수 있다.
5. 자동화된 측정 시스템 - 자동화된 테스트에 함께 붙이면 되는 경우가 많으며, 성능 측정과 잔존물 측정이 함께 이루어져야 한다. 성능 측정은 perfmon과 같은 외부 프로그램을 통한 측정과, 코드 수행속도 측정, 부가 데이터 측정 (로그나 DB 데이터 등...)등으로 분류된다. 측정 대상은 상황마다 다르지만, 보통 퇴근시간에 측정 시스템과 테스트 시스템을 켜놓은 후 다음날 결과를 바탕으로 회의와 업무를 진행하는 식으로 이루어지는 것이 좋다.
이렇게 갖춰놓는다면 아무리 사고뭉치라해도 개발 과정에서 대다수를 캐치 할 수 있습니다.
자 이렇게 좋은 개발팀이 버그를 많이 만들지 않도록 구축해야 할 것들에 대해 이야기해보았습니다. 어느정도 디버깅에 대한 이야기를 마무리 한거 같네요. 아쉬운 부분이 좀 있지만, 디버깅에 대해 좀 더 진지하게 다시 할 얘기가 생겼을 때 연재하도록 하겠습니다.
입체영상은 인간이 입체를 느끼는 요소를 인위적으로 만들어 내는 것이지요. 그러므로 우선 우리가 입체를 느끼는 원리를 이해해야 입체 영상을 다룰 수 있습니다. 이번 글에서는 이러한 입체 영상의 인식 및 구성 요소에 대하여 다루어보도록 하겠습니다.
입체시(stereo vision)
우리가 사물을 볼 때는 사물을 입체로 인식합니다. 가까운 사물은 가까이 느껴지고 멀리 있는 사물은 멀리 느껴지지요 (응? 당연한건가?) 이러한 인간의 입체 공간 인지 능력을 입체시(stereo vision)이라고 합니다. 이 입체시 능력은 인간의 눈이 두개인 것에서 출발을 합니다. 손바닥을 양 눈 사이에 세워놓고 오른쪽눈을 감고 왼쪽 눈으로 손바닥을 볼 때랑 왼쪽 눈을 감고 오른쪽 눈으로 손바닥을 볼 때랑 서로 다른 모양과 다른 면들 보게 되지요. 이처럼 각각의 눈에 다른 영상이 들어오는 것을 양안시차 혹은 양안부등(binocualr disparity)이라고 부릅니다. 이 양안시차를 뇌가 합성하여 입체 정보로 만들어 내고 이를 통해 입체시가 발현되는 것이지요. 이 양안시차는 두 가지 요소에 의해 만들어집니다.
세퍼레이션(separation)
먼저, 인간의 눈은 두 눈이 한 곳에 몰려있지 않고 어느정도 간격을 두고 떨어져있지요. 평균적인 성인 기준으로 두 눈은 6.5cm 정도 떨어져있는데요, 이를 안간(interocular)이라고 표현합니다. 두 눈이 떨어져서 다른 곳에 위치하고 있기 때문에 각각의 눈으로 다른 영상을 보는 것이지요. 입체 촬영 역시 이 안간을 흉내내어 카메라 렌즈 두 개를 어느정도 떨어트려 배치합니다. 이 카메라 혹은 렌즈 사이의 거리를 축간거리(interaxial distance)이라고 합니다.
이런 카메라 거리를 나타내는 또 다른 척도로 세퍼레이션이 있습니다. 세퍼레이션은 인터렉셜을 스크린 크기로 나눈 값을 의미합니다. 하지만 일반적으로 스크린크기는 상수이므로 보통은 카메라 사이 간격 조절을 세퍼레이션과 인터렉셜을 같은 의미로 용어가 혼용하여 사용되기도 합니다.
separation = interaxial / screen width
이 세퍼레이션은 사람이 입체 공간의 깊이를 인지하는데 영향을 미칩니다. 세퍼레이션의 갚이 작으면 즉 좌우 카메라 간격이 적으면 입체 3D 공간의 전체적인 깊이가 얕게 느껴지고, 세퍼레이션의 값이 크면 즉 좌우 카메라 간격이 많으면 입체 3D 공간의 깊이가 깊에 느껴지게 됩니다.
또한, 두 눈의 주시선은 무한히 평행하게 뻗어나가는 것이 아니라 어느 한 점에서 교차되게 되어있습니다. 그 교차점이 우리가 초점을 맞추는 대상이고 이를 폭주 또는 수렴(convergence)이라고 합니다.
입체 촬영 시 역시 이 컨버젼으를 흉내를 냅니다. 사람눈과 마찬가지로 두 카메라의 주시 방향을 평행하게 두는 것이 아니라 어느 한 지점에서 교차되게 배치를 합니다. 이 카메라의 시선이 교차되는 지점이 컨버젼스 포인트가 되는 것입니다. 즉 이 컨버젼스 포인트는 시차가 0이 되는 것이고 시청자가 볼 시 스크린 또는 모니터의 위치가 됩니다. 이 컨버젼스 포인트보다 앞에 있는 사물은 모니터 밖에 튀어나와 있게 느껴지는 것이고, 컨버젼스 포인트보다 뒤에 있는 사물은 모니터 안에 있게 느껴지는 것이지요.
이 세퍼레이션과 컨버젼스는 최종적으로 패럴렉스(parallax)라는 요소로 모니터에 투영이 됩니다. 이 패럴렉스는 모니터에 투영 된 좌안과 우안 영상의 수평 위치 차이를 의미합니다. 모니터보다 앞에 튀어나와 있도록 영상이 분리되어 있으면 음시차
(negative parallax)라 불리고 모니터 뒤에 있도록 영상이 분리되어 있으면 양시차(positive parallax)라 불립니다. 좌우 영상이 모니터상에 분리 없이 투영되어 있으면 딱 모니터 깊이에 위치가는 것이고 이는 무시차
(zero parallax) 상태가 됩니다.
다음 이미지는 컨버젼스와 세퍼레이션과 패럴렉스의 상관 관계를 한눈에 나타내줍니다. 빨간선은 좌안과 우안의 주시선을 타타냅니다. 주시선이 모이는 나무가 컨버젼스 포인트고 스크린 위치가 되는 것이지요. 컨버젼스에 수렴할수록 같이 세퍼레이션 값이 작아지는 것을 볼 수 있습니다.
요즘 디아블로 3를 즐기고 있는데 로그인 에러 때문에 빡치더군요. 돈 주고 게임을 샀는데 할 수가 없어요. 그러다 로그인 과정에 일정한 패턴도 있겠다. 한번 자동으로 만들어보자 해서 만들어보게 되었습니다. ( 이렇게 프로그래밍 스킬을 써먹는 것 이지요. )
열어줘!! 열어달라고!!!
일단 크게 3 부분이 필요하다는 것을 정리했습니다.
1. 전역 키 후킹
매크로 프로그램 같은 경우 게임의 배경에서 돌아가야 합니다. 포커스가 디아블로에 있다 하더라도 자동 로그인 프로그램이 on/off 될수 있도록 전역적인 키 후킹이 필요합니다. ( 예. F1로 자동 로그인 켜기, F2로 자동 로그인 끄기 )
2. 패턴화된 작업을 자동화
보통 로그인이 과정이 아이디는 입력해놓으면 고정되고, 패스워드 입력 -> 접속 버튼 클릭 -> (에러난 경우 ) 에러창 출력 -> 에러창 닫기 -> 패스워드 입력 -> ... 대략 이렇습니다. 이 과정만 자동화 해놓으면 되죠.
3. 화면 해상도
클릭 해야 될 부분이 해상도 마다 다릅니다. 모든 사람이 1920 * 1080 해상도를 쓰는 것이 아니므로, 클릭 좌표를 비율로 관리해야 했습니다. 일단 제가 사용하는 1680 * 1050 해상도를 기준으로 좌표를 클릭해야 할 좌표들을 얻은 다음 비율을 통해 대부분의 해상도에서 ( 와이드 기준 ) 작동 하도록 하였습니다.
// 후킹에 필요한 Win32API를 임포트
[DllImport("user32.dll")]
static extern IntPtr SetWindowsHookEx(int idHook, keyboardHookProc callback, IntPtr hInstance, uint threadId);
[DllImport("user32.dll")]
static extern bool UnhookWindowsHookEx(IntPtr hInstance);
[DllImport("user32.dll")]
static extern int CallNextHookEx(IntPtr idHook, int nCode, int wParam, ref keyboardHookStruct lParam);
[DllImport("kernel32.dll")]
static extern IntPtr LoadLibrary(string lpFileName);
// 후킹 프로시저 함수와 후킹 데이타 구조체 정의
delegate int keyboardHookProc(int code, int wParam, ref keyboardHookStruct lParam);
keyboardHookProc mkeyboardHookProc;
struct keyboardHookStruct
{
public int vkCode;
public int scanCode;
public int flags;
public int time;
public int dwExtraInfo;
}
// 후킹 후 불려질 콜백 함수 정의
public delegate bool CustomKeyEventHandler(Key k);
public event CustomKeyEventHandler CustomKeyDown;
public event CustomKeyEventHandler CustomKeyUp;
// 후킹 시작
const int WH_KEYBOARD_LL = 13;
public void hook()
{
IntPtr hInstance = LoadLibrary("User32");
mkeyboardHookProc = hookProc;
hhook = SetWindowsHookEx(WH_KEYBOARD_LL, mkeyboardHookProc, hInstance, 0);
}
// 후킹 끝
public void unhook()
{
UnhookWindowsHookEx(hhook);
}
// 후킹 프로시저
public int hookProc(int code, int wParam, ref keyboardHookStruct lParam)
{
if (code >= 0)
{
Key key = KeyInterop.KeyFromVirtualKey(lParam.vkCode);
// 등록된 키가 눌러졌으면
if (HookedKeys.Contains(key))
{
bool bHandled = false;
if ((wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN) && CustomKeyDown != null)
{
bHandled = CustomKeyDown(key);
}
else if ((wParam == WM_KEYUP || wParam == WM_SYSKEYUP) && CustomKeyUp != null)
{
bHandled = CustomKeyUp(key);
}
if (bHandled)
return 1;
}
}
return CallNextHookEx(hhook, code, wParam, ref lParam);
}
코드는 상당히 간단합니다. HookedKeys에 후킹 하고 싶은 키과 불려질 함수를 등록 해두고, 해당 키가 입력 되었을때 등록된 함수가 호출 되는 방식입니다. (KeyDown, KeyUp을 구분 해둠)
실제 사용하는 부분입니다.
// F1과 F2키를 후킹 하도록 한다.
// 위의 키가 눌리면 CustomKeyUpEvent가 호출
mGlobalHooker = new GlobalHooker();
mGlobalHooker.HookedKeys.Add(Key.F1);
mGlobalHooker.HookedKeys.Add(Key.F2);
mGlobalHooker.CustomKeyUp += new CustomKeyEventHandler(CustomKeyUpEvent);
이제 키 후킹은 되었으니 자동 로그인 부분을 만들어 보죠. 제가 생각하는 구조는 필요한 암호를 입력 받아놓고, 패스워드 필드에 패스워드 입력 후 ( 붙여넣기 ) 접속 버튼을 클릭. 그 다음 에러 창이 뜨고, 에러창 닫기를 클릭. 다시 패스워드 필드를 클릭 하고 ( 가끔 포커스가 아이디 필드로 날아가니 ) 다시 패스워드 입력 후... 이후 반복 작업입니다.
대략 4가지 일을 해야 합니다. 이 모든 일이 한번에 일어나면 이벤트 동작이 제대로 안됩니다. 이전 이벤트가 씹히기도 하죠. 그래서 각 이벤트를 일정 딜레이를 주고, 발생하도록 합니다. 일단 그전에 클릭 해야될 좌표를 구해야 합니다. 제가 1680 * 1050 해상도를 사용하는데, 이 해상도를 기준으로 클릭 좌표를 화면 해상도 비율로 구해놓았습니다. 그렇기 때문에 해상도가 변해도 클릭해야 될 곳을 정확히 클릭하도록 했습니다.
// 마우스와 키 입력 이벤트를 생성하기 위한 Win32Api 불러오기
[DllImport("user32.dll")]
static extern void keybd_event(byte vk, byte scan, int flags, int extrainfo);
[DllImport("user32.dll")]
static extern void mouse_event(uint dwFlags, uint dx, uint dy, uint dwData, int dwExtraInfo);
[DllImport("USER32.dll", CallingConvention = CallingConvention.StdCall)]
static extern void SetCursorPos(uint X, uint Y);
// 현재 화면 해상도(게임 해상도)를 비율로 구함
// 이 작업은 자동 로그인이 시작 될때 실행 됨
mScreenWidthRatio = 1.0 / 1680 * SystemParameters.PrimaryScreenWidth;
mScreenHeightRatio = 1.0 / 1050 * SystemParameters.PrimaryScreenHeight;
// 작업 이벤트를 순차적으로 등록
mEvents = new ArrayList();
mEvents.Add(new LoginEventHandler(LoginEvent1));
mEvents.Add(new LoginEventHandler(LoginEvent2));
mEvents.Add(new LoginEventHandler(LoginEvent3));
mEvents.Add(new LoginEventHandler(LoginEvent4));
// 이벤트 1 - 패스워드 필드 클릭
void LoginEvent1()
{
double CursorX = mScreenWidthRatio * 835;
double CursorY = mScreenHeightRatio * 686;
SetCursorPos((uint)CursorX, (uint)CursorY);
mouse_event(LBUTTONDOWN | LBUTTONUP, (uint)CursorX, (uint)CursorY, 0, 0);
}
// 이벤트 2 - 패스워드 붙여넣기
void LoginEvent2()
{
keybd_event(CTRLKEY, 0, 0, 0);
keybd_event(VKEY, 0, 0, 0);
keybd_event(CTRLKEY, 0, KEYEVENT_UP, 0);
}
// 이벤트 3 - 접속 버튼 클릭
void LoginEvent3()
{
double CursorX = mScreenWidthRatio * 835;
double CursorY = mScreenHeightRatio * 835;
SetCursorPos((uint)CursorX, (uint)CursorY);
mouse_event(LBUTTONDOWN | LBUTTONUP, (uint)CursorX, (uint)CursorY, 0, 0);
}
// 이벤트 4 - 에러37 확인 버튼 누르기
void LoginEvent4()
{
double CursorX = mScreenWidthRatio * 835;
double CursorY = mScreenHeightRatio * 615;
SetCursorPos((uint)CursorX, (uint)CursorY);
mouse_event(LBUTTONDOWN | LBUTTONUP, (uint)CursorX, (uint)CursorY, 0, 0);
}
각 이벤트들을 배열리스트에 담아두고 지정된 딜레이 시간마다 순차적으로 호출 되게 합니다. 여기서는 F1키가 입력되면 작업을 실행도록 합니다.
// 타이머를 생성
TimerClock = new DispatcherTimer();
TimerClock.Tick += new EventHandler(Timer_Tick);
// 등록된 키가 눌리면 불려지는 함수
bool CustomKeyUpEvent(Key k)
{
switch (k)
{
case Key.F1:
{
if (!TimerClock.IsEnabled)
{
// 해상도 비율로 사용 가능하게 비율을 구함
mScreenWidthRatio = 1.0 / 1680 * SystemParameters.PrimaryScreenWidth;
mScreenHeightRatio = 1.0 / 1050 * SystemParameters.PrimaryScreenHeight;
// 실행될 프로시저 인덱스
mCurrEventIDX = 0;
// 입력된 딜레이 값을 지정
// 입력된 패스워드를 클립보드에 복사
miDelayTime = Convert.ToInt32(textBox1.Text);
Clipboard.SetData(DataFormats.Text, textBox2.Text);
// 타이머 실행
TimerClock.Interval = new TimeSpan(0, 0, 0, 0, miDelayTime);
TimerClock.Start();
}
else
{
TimerClock.Stop();
}
}
return true;
// F2키는 종료. 종료 되기전에 클립보드를 비운다.
case Key.F2:
{
Clipboard.SetData(DataFormats.Text, "");
Application.Current.Shutdown();
}
return true;
default: return false;
}
}
// 타이머에 의해서 호출된다
// 호출될때마다 순차적으로 등록된 이벤트 함수를 호출한다
void Timer_Tick(object sender, EventArgs e)
{
if (0 == mEvents.Count)
return;
if (mCurrEventIDX >= mEvents.Count)
mCurrEventIDX = 0;
(mEvents[mCurrEventIDX] as LoginEventHandler)();
++mCurrEventIDX;
}
대략 이걸로 기본적인 동작은 하게 됩니다. 이렇게 만들어진 결과물~ 쨔란~ 알고 보면 별거 아님.