핵심 개념
- 블렌더 스크립팅
- 좌표계
- 유니티 에디터 스크립팅
https://github.com/applejin0105/Hitbox-Maker
GitHub - applejin0105/Hitbox-Maker: Hitbox-Maker Blender & Unity
Hitbox-Maker Blender & Unity. Contribute to applejin0105/Hitbox-Maker development by creating an account on GitHub.
github.com
총알이 날라오고 플레이어가 이를 맞는다. 분명 총알은 다리에 많이 맞았지만 이상하게 머리나 몸, 팔에 피격 판정이 많이 들어갔다면 일반적으로 플레이어는 이를 '버그'라고 생각할 것이다. 그렇다면 머리, 가슴, 팔, 다리에 히트박스를 붙여서 사용하면 이를 해결할 수 있을 것이다.
사고의 흐름은 다음과 같았다.
- 충돌 감지와 히트박스 로직을 분리한다.
- 충돌 감지는 커다란 IsTrigger가 True로 설정된 Capsule Collider로, 히트박스는 IsTrigger가 False로 설정된 각각의 Collider로 설정한다.
- Hitbox와 skeleton은 '보이지 않고, 투사체와 충돌하는지 여부만 판별'한다.
- Skeleton에 적용된 '애니메이션'에 맞추어 Hitbox가 자연스럽게 움직이고, 이에 따라 '특정 동작'을 수행하는 경우 피격 지점이 '자연스럽게' 변한다.
- 스켈레톤의 크기를 임의로 조절하여 여러 체형을 만든다.
즉, 히트박스와 충돌감지 캡슐을 제작하고 이를 자동으로 생성하는 로직을 구현하였다.
이렇게 두고 보았을때, 아무리 생각해도 유니티에서 노가다로 Collider를 만드는 것은 비효율적이고 차후 제작과정에서 수정이 필요할때 굉장히 힘들것이라 판단되었다. 남자와 여자의 기본적인 골반 크기 차이, 어깨 너비 차이등을 고려하고, 거기에 체격별로 스켈레톤을 조정하는 과정을 유니티에서 (유니티도 훌륭한 개발 툴이긴 하지만 3D 모델링만을 할 경우에는 아쉬운 부분이 지나치게 많았다.) 처리한다는 것은 애로사항이 많았다. 그렇기에, 블렌더와 유니티를 동시에 사용하고자 한다. 최종적인 결과물은 다음과 같다.


로직은 다음과 같다.
- 스켈레탈 메시를 준비한다. 여기에서는 mixamo의 Y Bot과 X Bot의 스켈레탈 메시만 추출하여 사용하였다.
- 스켈레탈 메시의 이름을 일괄 변경한다. mixamo는 기본적으로 mixamo:bone_name 형식이기 때문에 차후 사용할 다른 애니메이션 적용에도 에로사항이 있을 수 있어 변경하였다.
- 스켈레탈을 조절한다. 다음 표는 신장별 자연스러운 스켈레탈 메시에 대한 비율을 나타낸 것이다.
단순히 아마추어 전체를 스케일 조절하게 되면 거인, 소인이 되어버리기 때문에 Body 비율은 전체 스케일 조절, 그 뒤에 다리, 머리, 팔은 각각 하위 오브젝트를 전체 선택하여 적절한 위치에 기준점을 잡아 조절하였다.
가령 다리 조절시에는 Hips를, Head 조절시에는 Neck을, Arms 조절시에는 Spine2를 기준점으로 잡았다.
Legs Body Head Arms 0 0.65 0.7 1.4 0.75 1 0.7 0.78 1.3 0.8 2 0.85 0.85 1.2 0.9 3 0.9 0.9 1.15 0.95 4 0.95 0.95 1.1 0.975 5 1 1 1 1 6 1.05 1.05 0.975 1.025 7 1.1 1.1 0.95 1.05 8 1.15 1.15 0.925 1.075 9 1.2 1.2 0.9 1.1 10 1.3 1.3 0.85 1.15 - 이렇게 만들어진 스켈레톤은 수동으로 저장했다. (추후에 스켈레톤 자동 수정 코드도 짤 예정이다.)
- Hitbox_Maker를 실행한다.
- 뼈에 맞추어 히트박스를 생성한다. 이때, 플레이어의 충돌을 감지할 PlayerCapsule 그리고 플레이어가 땅에 붙어있는지를 검사할 GroundChecker Sphere를 추가적으로 생성한다.
- 다음 세팅에 맞추어 FBX를 Export 한다.

- 유니티에서 해당 FBX를 가져와서, 상단에 새로 생긴 Custom -> HitBox -> Create Hit Box를 선택한다.

- 이렇게 하면 자동적으로 히트박스에 Collider를 붙이고, Mesh Renderer를 지우며 GroundChecker를 비롯한 여러 로직을 수행한다. 히트박스에는 태그도 자동적으로 생성하며, 만일 태그가 없다면 태그도 생성하여 붙이게된다.
이렇게 완성되기까지 중요하게 공부한 부분에 대해 설명한다.
- 에디터 스크립팅, Static
MenuItem 어트리뷰트로 Unity 메뉴에 새로운 항목을 추가할때는 반드시 static으로 선언해야한다. 당연하게도 메뉴에 딱 하나, '전역적'으로 등록해야 하기 때문이다. 이 때문에 여러 함수와 변수를 사용하여 작성할 경우, 당연하게도 모두 static, 전역으로 선언해야한다.
[MenuItem("Custom/HitBox/Create Hit Box")] private static void Init() { if (Selection.activeTransform == null) { Debug.LogWarning("Root GameObjet를 선택하십시오."); return; } List<string> tags = new List<string>() { "CharacterSkeleton", "Hitbox_Head", "Hitbox_Torso", "Hitbox_Arm", "Hitbox_Leg", "GroundChecker"}; foreach (var t in tags) { AddTag.InitTag(t); } _target = Selection.activeTransform.gameObject; CreateHitBox(); GameObject playerCapsule = FindDeepChild(_target, "PlayerCapsule"); GameObject groundCheckerL = FindDeepChild(_target, "GroundChecker_L"); GameObject groundCheckerR = FindDeepChild(_target, "GroundChecker_R"); EndSetting(_target, playerCapsule, groundCheckerL, groundCheckerR); } - 블렌더 좌표계 사용
작성한 블렌더 코드를 자세히 본다면, 전부 world 좌표계를 기반으로 생성하고, 이를 '유지' 한 상태로 Bone에 Parent로 붙이게 된다. 만일 이를 유지한 상태로 Bone에 붙이지 않는다면 좌표가 뒤틀리게 된다. 또한 블렌더와 유니티 좌표계 상에 차이가 있다는 것은 확실하게 인지해두어야 한다
# worldLocation을 기반으로 히트박스의 생성 위치를 결정하는 로직
def worldLocation(BONE, ISHEAD):
if ISHEAD == True:
return arm_obj.matrix_world @ BONE.head_local
else:
return arm_obj.matrix_world @ BONE.tail_local
# Pose 모드로 전환 뒤, keep_transform=True를 통해 위치를 고정하여 뼈에 붙인다.
def makeParentBone(PARENT_BONE_NAME, MESH_NAME):
mesh = bpy.data.objects.get(MESH_NAME)
if mesh is None : printError(MESH_NAME)
setToPose()
for b in arm.pose.bones:
b.bone.select = False
bone = arm.pose.bones.get(PARENT_BONE_NAME)
if not bone: printError(PARENT_BONE_NAME)
bone.bone.select = True
armature.bones.active = bone.bone
mesh.select_set(True)
arm.select_set(True)
bpy.context.view_layer.objects.active = arm
try:
bpy.ops.object.parent_set(type='BONE', keep_transform=True)
print(f"'{MESH_NAME}'를 '{PARENT_BONE_NAME}'에 부착하였습니다.")
except RuntimeError as e:
print(f"'{MESH_NAME}'를 '{PARENT_BONE_NAME}'에 부착하지 못했습니다.")
setToObject()
특히 특정 뼈를 붙이기 위해서는 반드시 Pose 모드로 전환해야한다.
이번 작업을 통해 다음과 같은 결과를 얻을 수 있었다.
- 유니티 에디터 스크립팅 지식 확립
- 블렌더 스크립팅 지식 확립
- 엔진에서 사용하는 좌표계에 대한 이해
- Bone, Skeleton, Animation의 기본적인 작동 원리
이 지식을 기반으로, 추후에는 Skeleton을 체형별로 자유롭게 조절 가능한 블렌더 스크립트를 작성할 것이며, 히트박스 기반으로 투사체 충돌 과정을 처리할 예정이다.