요즘 저희 Cafe에서 기획중인 게임에는 아마도 게임패드와 키보드를 둘 다 지원하는 인터페이스가 들어 갈 것이라고 생각하는데요. 특히 게임 패드인 경우 진동 기능을 넣지 않을 수가 없습니다. 게임 패드의 진동기능은 정말 대단한 몰입감을 줍니다.
이번에는 진동기능과 관련되어서 꼭 알아야될 정보와 쉽게 사용가능하도록 만든 초간단 클래스도 같이 공개하도록 하겠습니다.
먼저 아래의 내용은 MSDN 내용과 제가 XBOX360 게임 개발시에 삽질한 것 내용을 정리한 것입니다. ^^;
XBOX360게임은 FPS가 25~30 사이를 왔다갔다 했습니다. 제작한 게임이 풀 3D게임이여서 60 Frame으로 돌리기에 벅찼습니다.
(한 화면에 3만~10만 폴리곤을 왔다갔다 했습니다. 이러다보니, 성능과 상황에 따른 진동처리 문제가 좀 더 현실적으로 다가 왔었습니다.)
<진동에 대한 이해>
1. GamePad 클래스 내부의 SetVibration 함수를 호출해서만 접근이 가능합니다. (이 외에는 방법이 없음)
2. 한번 SetVibration 함수를 호출하면, 중지 명령을 내리기 전까지 무조건 지속됩니다.
3. 중지명령을 내리는 것도 SetVibration을 통해서 가능. -> 진동 수치를 0.0으로 설정하면 됩니다.
4. 패드를 중심으로 왼쪽은 Low frequency, 오른쪽은 High frequency 입니다.
-> 보통 진동을 그냥 0.5,0.5 이렇게 주면 왼쪽 오른쪽 벨런스하게 진동이 올 것이라고 생각하지만, 실은 용도가 다릅니다.
-> 일단 (0.5,0.0) 이렇게 입력을 주면, 왼쪽의 Low frequency한 모터가 움직입니다. 이 의미는 심장박동 느낌처럼 진동의 강도와 주기가
강도는 강하고, 주기는 긴 느낌입니다. 반면 (0.0,0.5) 이렇게 입력을 주면, 면도기 진동 처럼.. 빠르고 진동의 감도가 약합니다.
-> 기어스오브워 같이 톱질 할때 수치를 생각해보면, 대략 (1.0, 0.6) 정도 인 것 같습니다. (추측)
-> 이 느낌은 밑에서 모터는 나름 빨리 돌고 있는 것이고, 위에서는 막 갈려고 힘이 들어가는 느낌이라고 할까요? ㅋㅋ
5. SetVibration함수가 항상 true가 되는 것이 아니라는 것도 매우 중요합니다. (얼마 전 게시판에서 나온 사운드 관련 질문과 같은 맥락입니다.)
-> 특히 진동인 경우 중간에 끊기게 되면, 손의 감각에 의해 금방 탄로가 납니다.
-> SetVibration함수가 일단 중첩되어서 들어가게 되면, false가 리턴될 확률이 높아집니다. (다른 것도 충분히 부하가 있는 상황을 가정)
-> 그리고 가비지 컬렉션이 일어나고 있는 도중이나, LOAD/SAVE 등 디스크 IO에 접근할때 진동기능을 호출하면, 끊길 확률이 높습니다.
-> Help 내용을 보면." Setvibration 함수의 리턴값이 true가 되도록 루프에서 wait하면서 기다리는 것은 추천되지 않습니다." 라고 되어있는데, 즉, 이 경우 다음 번 loop에서 다시 시도해야 됩니다. loop이 돌아오면서, IO의 pending상태나 이런 것들이 재 설정 될 수 있기 때문입니다.
-> 다행인 것은 Light한 게임을 만들때는 5번 케이스가 거의 안나온다는 것입니다. 하지만 XBOX360의 경우는 가비지 컬렉션 타임에 걸리면, 생각보다 fail이 잘 납니다. 또한 이 경우는 진동중지 명령이 바로 되는 것이 아니므로, 진동 상태로 1초 동안 머물러 있을 때도 있습니다. ^^;
6. 마지막으로 진동 호출 함수는 가능하면 Update 함수에서만 처리되어야 합니다. Draw 함수보다는 Update에 넣는 것이 더 안정적입니다. Update -> Draw 시, Update와 Draw 중간에서 IO 처리가 일어날 것입니다. 이때 Draw는 보통 부하가 크기 때문에 호출했다고 해도, Update때 보다는 느리므로, 진동이 lack이 걸리는 것 처럼 올 수도 있습니다.
<따라서 추가로 필요한 기능>
1. 시간관리 기능이 하나 필요합니다. (몇 초 동안 만 진동해라.. 이런 것이죠.)
2. 진동 강도 조절 기능이 필요합니다. (1.0의 강도가 진행되고 있었는데, 0.1 강도가 들어왔다고, 0.1로 설정하면 안됩니다.)
<실제 구현 코드>
위의 추가로 필요한 기능까지를 포함해서, 새로운 클래스를 2개를 만들었는데요. 아래와 같습니다. (초 간단입니다. ^^;)
<cVibration.cs>
// cVibration의 list에서 사용되는 data.
publicclasscVibrationData
{
publicfloatm_Duration;
publicfloatm_LeftMoter;
publicfloatm_RightMoter;
publiccVibrationData(floatduration_ms,floatleftMoter,floatrightMoter)
{
m_Duration=duration_ms;
m_LeftMoter=leftMoter;
m_RightMoter=rightMoter;
}
}
publicclasscVibration
{
privatefloatm_TotalLeftMoter;
privatefloatm_TotalRightMoter;
privateList<cVibrationData>m_listVibration;
#regionTest를 위한 property
publicfloatTotalLeftMoter_ForTest
{
get{returnm_TotalLeftMoter; }
}
publicfloatTotalRightMoter_ForTest
{
get{returnm_TotalRightMoter; }
}
#endregion
~cVibration()
{
m_listVibration.Clear();
m_listVibration=null;
}
publiccVibration()
{
m_TotalLeftMoter= 0.0f;
m_TotalRightMoter= 0.0f;
m_listVibration=newList<cVibrationData>();
}
publicboolAddVibration(floatduration_ms,floatleftMoter,floatrightMoter)
{
intbeforeCount=m_listVibration.Count;
// add함수는 리턴값이 void이기에 true/false를 판단하기 위해, 갯수로 판단함.
m_listVibration.Add(newcVibrationData(duration_ms,leftMoter,rightMoter));
if(beforeCount+ 1 ==m_listVibration.Count)
{
// 일단 시작해야 함. 시작하지 않으면, 일정 시간이 지나가 버림.
constfloatdefaultTime= 0.0f;
DoUpdate(defaultTime);
returntrue;
}
returnfalse;
}
privatevoidDoUpdate(floatupdatedTime)
{
floattempTotalLeft= 0.0f;
floattempTotalRight= 0.0f;
for(inti= 0;i<m_listVibration.Count;i++)
{
// 시간 체크 루틴.
m_listVibration[i].m_Duration=m_listVibration[i].m_Duration-updatedTime;
if(m_listVibration[i].m_Duration> 0.0f)
{
if(tempTotalLeft<m_listVibration[i].m_LeftMoter)
tempTotalLeft=m_listVibration[i].m_LeftMoter;
if(tempTotalRight<m_listVibration[i].m_RightMoter)
tempTotalRight=m_listVibration[i].m_RightMoter;
}
else
{
m_listVibration.RemoveAt(i);
i--;// m_listVibration.Count 값이 하나 작아짐., 다시 i번째를 수행하게 한다.
}
}
// 기존 값과 값을 경우, SetVibration을 할 필요가 없다.
if(tempTotalLeft!=m_TotalLeftMoter||tempTotalRight!=m_TotalRightMoter)
{
m_TotalLeftMoter=tempTotalLeft;
m_TotalRightMoter=tempTotalRight;
GamePad.SetVibration(Microsoft.Xna.Framework.PlayerIndex.One,tempTotalLeft,tempTotalRight);
}
}
publicvoidUpdate(floatupdatedTime_ms)
{
DoUpdate(updatedTime_ms);
}
}
자, 실제 사용을 해보겠습니다.
1. 선언
publicclassGame1:Microsoft.Xna.Framework.Game
{
GraphicsDeviceManagergraphics;
SpriteBatchspriteBatch;
cVibrationm_Vibration;
}
2. 초기화
protectedoverridevoidInitialize()
{
m_Vibration=newcVibration();
base.Initialize();
}
3. 사용 및 업데이트
protectedoverridevoidUpdate(GameTimegameTime)
{
// Allows the game to exit
if(GamePad.GetState(PlayerIndex.One).Buttons.Back==ButtonState.Pressed)
this.Exit();
if(GamePad.GetState(PlayerIndex.One).Buttons.A==ButtonState.Pressed)
{
constfloatduration_ms= 200;
constfloatleftMoter= 0.5f;
constfloatrightMoter= 0.5f;
m_Vibration.AddVibration(duration_ms,leftMoter,rightMoter);
}
if(GamePad.GetState(PlayerIndex.One).Buttons.B==ButtonState.Pressed)
{
constfloatduration_ms= 200;
constfloatleftMoter= 0.75f;
constfloatrightMoter= 0.75f;
m_Vibration.AddVibration(duration_ms,leftMoter,rightMoter);
}
// 이렇게 Update를 호출해 주면 됩니다.
m_Vibration.Update((float)gameTime.ElapsedGameTime.TotalMilliseconds);
base.Update(gameTime);
}
<추가 프로젝트>
cVibration이 잘 작동하는지, 또 제 의도가 명확했는지 테스트 하기 위해서, 테스트 프레임 워크(NUnit)를 사용했습니다. 실제 Test코드를 보시면 어떤 의도로 작업을 했는지 명확하게 확인하실 수 있습니다. (cVibration_Test.cs)
특히 '사과'님 께서 일전에 물어보셨던 TDD와 관련된 내용을 넣으려고 조금 더 평소보다 디테일 하게 했으니 참고하세요 ^^;