탄창에 총알을 채웠으니 이제 방아쇠를 당겨야 할 차례입니다. 발사된 총알은 총구를 통해 날아가게 되지만 무조건 총구가 향한 방향으로 날아가진 않습니다. 현실에서는 총알의 종류나, 강선의 회전, 총기에서 발생한 진동 등 여러가지 요인으로 인해 총알의 비행을 방해하기 때문이며, 이를 명중률이라고 합니다.
컴퓨터 세상 속에서는 환경 요인의 영향을 받지 않기 때문에 직선으로 날아가게 만들면 거의 직선으로 날아가게 되겠지만, 게임의 밸런스 또는 좀 더 사실적인 연출을 위해서 의도적으로 총알이 빗나가도록 구현하기도 합니다. 이러한 기능들은 플레이어가 총을 선택하는 중요한 지표가 되기도 하며, 총의 종류를 분류하기 위해 사용됩니다.
명중률을 구현하기 전에 먼저 총알을 일직선으로 잘 발사하기 위한 사전 작업을 진행하였습니다.
해당 사진은 무기 아이템 중 하나의 Hierarchy입니다. 자식 오브젝트에 Muzzle이라는 Empty 오브젝트를 추가하여 총알이 발사되는 위치를 지정해주었습니다. 자식 오브젝트로 지정하였기 때문에 별도의 스크립트 없이 총의 이동과 회전에 따라 자동으로 총구의 위치가 지정될 것입니다.
"TGItemWeapon" 스크립트에는 Inspector를 통해 총구와 발사체(총알)를 담당할 오브젝트들을 지정할 수 있도록 구현하였습니다.
이때 발사체는 물리 효과를 구현하고, 충돌 감지를 위해서 Rigidbody와 "TGProjectile" 컴포넌트를 사용합니다. 또한, 해당 발사체를 발사하기 위해 이전에 구현하였던 ObjectPool을 사용하여 활성화 및 생성하게 됩니다. 활성화 된 이후에는 Rigidbody를 이용하여 가속, 직선으로 이동하게 됩니다.
Rigidbody를 통해 물리 연산을 하는 오브젝트를 다룰 때에는 주의해야 할 점이 있습니다. 이 문제는 Unity Lifecycle과 밀접한 관계가 있습니다. Unity에서는 물리 연산을 할 때 고정적으로 60 프레임을 사용하기 때문에, 이를 고려하지 않고 Position을 옮기게 되면 오류가 발생할 수 있습니다.
사진 출처: 유니티 코리아 공식 유튜브 채널 https://youtu.be/QtmGT-22PqA
먼저 이 문제를 이해하기 위해서는 유니티가 어떤 구조를 갖고 동작하는지 이해해야 합니다. 유니티는 위 사진 처럼 3개의 부분으로 나뉘어서 연산을 진행하는데, 우리가 작성하는 C# 스크립트들은 C++로 작성된 엔진을 제어하기 위한 API를 호출하는 것이고, Physics는 물리 연산을 위해 따로 작동하는 구조를 갖고 있습니다. 이때, 물리 연산을 위한 부분은 늘 60fps으로 고정되어 연산을 진행하게 되는데 60fps보다 더 많은 새로고침 빈도를 갖는 곳에서 position을 변경하게 된다면 물리 연산을 하는 부분과 엔진 사이에서 position에 대한 괴리가 발생하게 될 것입니다. 이때, 오류가 발생하여 애플리케이션이 종료되는 것을 막기 위해 position을 다시 default로 변경하는 것이 아닌가 추측됩니다. 해당 추측에 대한 근거로 Log를 통해 position 값을 출력하였을 때, 정상적으로 출력되는 점 등이 있습니다.
소스코드로 예를 들어보겠습니다.
public void CommandFire(Vector3 muzzlePosition, Quaternion muzzleRotation, float velocity) // 발사가 호출됐을 때
{
transform.rotation = muzzleRotation;
transform.position = muzzlePosition;
rb.velocity = transform.forward * velocity;
isFlying = true;
Debug.Log($"(TGProjectile:CommandFire) muzzlePosition: {muzzlePosition}, ProjectilePositino: {transform.position}");
Invoke("ReleaseProjectile", releaseTime);
}
위 소스코드는 발사체가 활성화 된 직후, 같은 프레임에서 발사체의 위치와 회전을 muzzle의 transform으로 변경한 후, 가속을 줘 muzzle이 바라보는 방향으로 비행하도록 합니다. 이때 기존의 방법처럼 발사체의 position을 변경하면 물리 연산을 담당하는 부분과 C# 스크립트를 담당하는 부분의 초당 프레임 수가 다르기 때문에, 위치를 연산하는데 오류가 발생한다고 생각합니다.
Unity에서는 이를 해결하기 위해 Rigidbody에 여러 메소드를 제공하고 있습니다. 이 중에서 position 값을 제어하기 위해서는 RigidBody의 MovePosition() 메소드를 사용해야 합니다. 아래 영상은 MovePosition()을 적용했을 때입니다.
또한, 같은 이유로 물체를 움직일 때 Trnaslate()같은 메소드를 사용하면 충돌 처리를 할 때 물체를 뚫고 지나가는 오류가 발생할 수 있기 때문에 Rigidbody의 velocity를 제어하도록 해야 합니다.
문제의 원인은 Unity 엔진의 설계 변경에 있었습니다. 기존 Unity 엔진에서는 Transform 값이 물리를 포함한 다른 컴포넌트에도 영향을 주도록 설계되어 있었는데 2022 업데이트 이후 Transform 값과 독립되도록 설계를 변경했습니다. 따라서, Rigidbody 같은 컴포넌트를 통해 물리연산을 수행하는 오브젝트들은 명시적으로 Transform의 Positon 변경과 Rigidbody의 MovePostion을 동시에 사용해야 한다는 것을 알게 되었습니다. 해당 문제를 해결하기 위해 위 방법을 코드에 적용하였고, 문제를 해결할 수 있었습니다.
해당 프로젝트에서는 Json을 통하여 총기의 능력치를 쉽게 지정할 수 있도록 구현하였습니다. 이 때, 명중률도 지정할 수 있는데 currentAccuracy는 게임 환경에 따라 유동적으로 변화할 수 있는 값입니다.
명중률을 구현할 할 때에는 회전을 이용하였습니다. muzzle의 rotation값에서 currentAccuracy 값만큼의 랜덤한 회전을 주어서 발사체가 사실적으로 비행할 수 있도록 하였습니다. 아래는 명중률 계산 소스코드입니다.
private Quaternion AccurateCalc(float accuracy) // 명중률 계산 메소드
{
float xRotation = Random.Range(-accuracy, accuracy);
float yRotation = Random.Range(-accuracy, accuracy);
Quaternion deltaRotation = Quaternion.Euler(xRotation, yRotation, 0f);
return transform.rotation * deltaRotation;
}
비행체가 랜덤한 지점에 착탄한 사실을 알 수 있습니다.