ZombieSurvival 시작한날 정리
20261023 ZombieSurvival 시작
LightMap 생성
3D게임은 LightMap을 구워야 한다(고정된 계산에 대한 연산 감소로 성능향상) Project 폴더위치를 변경하거나 새로 받으면 한번 구워줄 것. Window -> Rendering -> Lighting(Ctrl + 9) 열기 Lighting Settings의 Lighting Settings 항목의 new버튼 클릭. 화면 하단 Generate Lighting버튼으로 LightMap을 생성한다.
- 기존 LightMap이 있으나 Project폴더 이동 등으로 인하여 연결이 깨졌거나 사용하지 않는 LightMap은 Clear Baked Data를 통해서 먼저 삭제 해준 후 생성한다. Clear Baked Data는 Generate Lighting버튼 옆의 화살표로 확장하면 보이며 실행하면 Unity가 사용하지 않는 LightMap을 자동 확인 후 삭제해준다.
Player 생성
base로 받은 Asset에서 Models의 Woman을 Player로 사용할 것인데 Model을 그대로 GameObject로 만들어서 사용하면 안되고 GameObject로 만든 후 다시 Prefab으로 만들어야함.
- 빈 GameObject를 만든 후 그 자식으로 Woman을 넣어서 Player를 생성. 이후 Prefab으로 만들면 된다.
Skinned Mesh Renderer
Woman GameObject를 확인해보면 Component에 Skinned Mesh Renderer가 있는데 이 Component의 역할은 다음과 같다. 캐릭터가 움직임에 따라서 Mesh에는 늘어나고 줄어드는 부분이 생기는데(예를들어 팔꿈치 같은 부분들) 이런 과정에서 이 모습이 끊어져 보일 수 있다. 이에 맞춰 늘어나고 줄어들어 공간이 비어보이지 않도록 Rendering 해주는 Component이다.
Player 생성시 참고사항
- 일반적으로 root 오브젝트를 그대로 쓰는것이 아니라 빈 GameObject를 생성 후 자식 오브젝트로 해서 넣는다.
- 보통 pivot을 발끝으로 준다. Player의 몸통 가운데에 pivot을 두면 크기를 키우거나, 줄이거나, 이동할 때 문제가 발생할 확률이 높다.
- Collider는 이번에 Capsule Collider를 사용한다. 캐릭터의 외형에 맞춰충돌을 확인하는 Mesh Collider가 있지만 연산량이 너무 크고 그만큼의 정확도를 보장하지 않을때가 있다.
Input사용시 리터럴문자 사용에 대하여
사용자의 입력을 받기위한 Input 사용시 "Vertical", "Horizontal" 같은 입력을 Input.GetAxis("Horizontal") 이런식으로 리터럴로 직접 입력하는것은 좋지않은 방식이다. 변수로 선언 후 사용하는 방식을 권장한다.
// 변수 선언 후 사용하는 방식 권장
public static readonly string MoveAxis = "Vertical";
public static readonly string RotateAxis = "Horizontal";
public static readonly string FireButton = "Fire1";
public static readonly string ReloadAxis = "Reload";
public float Move { get; private set; }
public float Rotate { get; private set; }
public bool Fire { get; private set; }
public bool Reload { get; private set; }
private void Update()
{
Move = Input.GetAxis(MoveAxis);
Rotate = Input.GetAxis(RotateAxis);
Fire = Input.GetButton(FireButton);
Reload = Input.GetButtonDown(ReloadAxis);
}
Animation Blend Tree
수치별로 여러개의 애니메이션을 섞어서 부드럽게 동작을 전환하는 기능이다.
Run <-> Walk <-> Idle <-> Walk <-> Run Move 수치에따라(-1 ~ 1) 각 동작의 비율을 섞어가며 자연스럽게 움직이는것 처럼 보이도록 할 수 있다. base로 받은 파일에서는 ShooterAnimator의 Base Movement에 있는 Movement를 더블클릭하여 확인할 수 있다.
Animator의 Apply Root Motion
GameObject의 Animator Component에 있는 Apply Root Motion옵션은 On 상태이면 Animation에 적용되어있는 회전값이다 기타 수치들 까지 실제 Play 중의 이동량이나 회전량에 반영이됨. 그냥 켜놓기만 해도 Animation이 좌, 우회전이 있다면 아무런 조작을 하지 않아도 캐릭터가 혼자서 좌, 우로 방향을 바꾸거나 하는 모습을 보임. Off상태여야 플레이어의 조작만 반영해서 움직일 수 있음.
FixedUpdate와 Update
- Update: RenderingUpdate라고 보면 된다. 성능이 허용하는한 계속 호출되고 한 Frame당 호출이기 때문에 기기의 상태나 성능에 따라서 호출되는 속도가 들쭉날쭉함.
- FixedUpdate: 물리 Update라고 보면 되고 주기가 update랑 다름. 매 Frame별로 Update해야하는 경우는 Update 함수에서 진행하며 물리관련된 Update는 FixedUpdate 함수에서 (이동, 회전같은거)진행하는 것이 좋다. 사용자입력(Input)에 대한 Update 같은건 FixedUpdate에서 하면 누락될 수 있음. 때문에 사용자 입력에 대한 Component를 따로 처리하고 Movement에 대한 Component를 따로 처리한다.
Animation의 값 조정 역시 리터럴이 아닌 변수선언으로 처리할 것
// 움직임에 대한 Animation 처리도 변수 선언으로 받는 예시
public static readonly int HashMove = Animator.StringToHash("Move");
private void Update()
{
playerAnimator.SetFloat(HashMove, playerInput.Move);
}
Animation Layer와 Mask
base로 받은 asset을 보면 Animations에 Animator항목과 Clip들. 그리고 Mask라고 되어있는 파일을 확인할 수 있다. Layer가 두개 이상일 경우 두 기본 Animation에 Mask가 적용된 Animation을 덮어씌워서 실행할 수 있다. asset중 UpperBodyMask의 Inspector를 보면 녹색으로 표시된 부분과 붉은색으로 표시된 부분이 있는데 녹색으로 표시된 부분만 기존 Animaion에 덮어씌워서 사용할 수 있다. BaseMovement는 기본 이동(걷기, 뛰기)에 대한 Animation이고 UpperBodyMask는 총을 들고있는 Animation이다. 다리는 이동을 하면서 총을 들고있는 모습처럼 두가지 이상의 Animation이 섞이게 만들고 싶을 때 Layer와 Mask를 사용한다.
캐릭터의 회전
캐릭터의 회전은 실수 4개를 써서 한다. Vector3를 쓰면 문제가 발생하기 때문에 Quaternion(4원수)을 사용.
Player를 따라다니는 카메라
Camera로 Player를 따라가게 하는 방법은 두 가지 정도가 있다.
- Camera용 Script를 만들어서 Player의 정보를 받아서 직접 따라다니도록 하는 방법.
- Cinemachine을 이용해서 만드는 방법.(이번에는 이 방법을 사용)
- Cinemachine의 Virtual Camera를 사용해본다. Virtual Camera를 사용하기 위해선 해당하는 Package를 설치해야 한다. Window -> PackageManagement -> PackageManager -> Unity Registry에서 Cinemachine을 검색. 설치할 Version을 다운받으면 되는데 이번에 사용할 Virtual Camera는 2.10.7 Version에 있으며 3.x.x Version에는 Cinemachine Camera로 이름이 변경되었다. Virtual Camera 생성 후 Inspector의 설정을 해줘야 한다. Follow: Camera 위치(Camera가 따라다닐 대상. 현재는 Player) Look at: Camera가 바라보는 방향(현재는 Player) Lens Vertical FOV 시야각 설정 20 Body -> Binding Mode를 World Space로 설정 Follow Offset: X는 -8 Y는 16 Z는 -8 로 설정 X, Y, Z Damping: 특정한 목표값까지 선형으로 이동할 때 값에따라 이동 속도를 조절함. Aim -> Tracked Object Offset X, Y, Z : 기본적으로 Camera가 Player의 pivot이 발로 되어있기 때문에 발을 바라보게 되어있음. 이걸 조정해줘야 한다. Y를 0.5로 조정.
ScriptableObject
GunData에 대하여 ScriptableObject를 상속 받아 생성해본다.
먼저 ScriptableObject를 생성해야한다.
Project 생성 -> Scripting -> ScriptableObject Script를 생성한다.
ScriptableObject를 상속받은 Script가 생성되고 상단에 [CreateAssetMenu(fileName = "GunData", menuName = "Scriptable Objects/GunData")]가 들어가 있다.
이것은 생성자랑 같다고 보면 되는데 ScriptableObject 를 상속받아 만든 ClassData를 가진 Asset을 생성할 수 있다.(Project에서 Create -> Scriptable Objects -> GunData로 생성가능)
LineRenderer
Gun object에 Add Component -> LineRenderer 추가. 총알이 나가는 것 처럼 보이도록 하기 위해서 총을 발사할때마다 켰다 껏다 할 예정이다.
Gun Effect 추가
Gun object의 자식으로 추가 Effect들을 추가한다. MuzzleFlashEffect: 총알이 나갈때 총구에서 불이 번쩍 하는 이펙트 ShellEjectEffect: 총알이 나갈때 탄피가 떨어지는 이펙트
// Gun Script에 ParticleSystem 추가및 사용 예시
public ParticleSystem muzzleFlashEffect;
private void Shoot()
{
muzzleFlashEffect.Play();
}
Enum문 사용
Enum문을 사용해서 현재 총을 발사중인지, 총알이 없는 상태인지, 재장전중인지에 대한 상태를 저장할 수 있다.
// Enum문을 통한 상태 표현 예시
public enum State
{
Ready,
Empty,
Reloding,
}
이를 통하여 각 상태에 맞지 않는 행동은 할 수 없게하거나 혹은 상태 변경시에 해당 상태에서 해야하는 동작들에 대한 설정을 보기 편하도록 작성할 수 있다.
Ray
시작점만있고 끝이 없는 한쪽방향 직선을 Ray라고 한다.
Ray 생성: Ray ray = new Ray(시작점, 방향)
Ray로 충돌확인: if(Physics.Raycast(ray, out RaycastHit hit, 제한거리, targetLayer))
targetLayer를 설정해서 넣어주면 해당 Layer가 설정되어있는 일부 obj만 충돌 검사를 하는것이 가능하다. (예를들어 멀티플레이인 경우 다른 플레이어를 제외한다던가, 발사위치가 자신과 겹쳐있다면 자신은 빠지도록 하던가 등등)
// Ray를 통해 충돌을 확인하는 방법 예시
private void Shoot()
{
Vector3 hitPosition;
Ray ray = new Ray(fireTransform.position, fireTransform.forward);
if(Physics.Raycast(ray, out RaycastHit hit, fireDistance, targetLayer))
{
hitPosition = hit.point;
IDamageable target = hit.collider.GetComponent<IDamageable>();
if(target != null)
{
target.OnDamage(gunData.damage, hit.point, hit.normal);
}
}
else
{
hitPosition = fireTransform.position + fireTransform.forward * fireDistance;
}
}
Coroutine
논리적 비동기 흐름. Thread랑은 다른것이다. Thread는 기기의 multiCore로 인하여 실제로 동시 실행이 가능하지만 Coroutine은 단일 Thread에서 진행한다. 논리적 비동기 흐름이라 동시해 진행되는 것 처럼 보일 뿐 실제 동시실행은 아니다. 유니티는 multi Thread를 부분적으로만 지원한다.(파일 I/O, 네트워크 통신, 복잡한 순수 수학/데이터 연산 등 유니티 API와 무관한 작업) 핵심로직(게임 로직, 렌더링, UI, 물리 연산 등)은 단일 Main Thread에서 진행한다. 오래된 시스템이라 사용이 점점 없어지는 추세이다. 외부 오픈소스 라이브러리인 유니테스크(UniTask)나 유니티에 기본 내장된 비동기 클래스 어웨이터블(Awaitable)을 쓰는데 어웨이터블은 잘 안쓰는 것 같다.
- 별도 검색 내용 유니테스크와 어웨이터블의 장단점 유니테스크: 별도 설치가 필요하지만, 실무 프로덕션 환경에서 여전히 가장 강력하고 표준처럼 쓰인다. 어웨이터블: 외장 패키지를 안 깔아도 되는 공식 기능이지만, WhenAll/WhenAny 같은 유틸리티가 부족하고 동일 인스턴스를 중복 await할 수 없는 등의 제약이 있어 아직은 UniTask보다 기능이 단순하다.
Corutine에 사용할 함수 선언
private IEnumerator CoShotEffect(Vector3 hitPosition)
IEnumerator는 나중에 별도 수업 예정임.
// Corutine으로 실행할 함수. Effect랑 Sound, BulletLine을 켰다가 끄는것.
// 관행적으로 코루틴에 들어갈 함수 이름은 앞에 Co를 붙여준다.
private IEnumerator CoShotEffect(Vector3 hitPosition)
{
muzzleFlashEffect.Play();
shellEjectEffect.Play();
gunAudioPlayer.PlayOneShot(gunData.shotClip);
bulletLineRenderer.SetPosition(0, fireTransform.position);
bulletLineRenderer.SetPosition(1, hitPosition);
bulletLineRenderer.enabled = true;
yield return new WaitForSeconds(0.03f);
bulletLineRenderer.enabled = false;
}
포워드 키네마틱 FK(Forward Kinematics, 순운동학)과 인버스 키네마틱(IK, Inverse Kinematics, 역운동학)
Model의 Inspector에 있는 Rig -> Animation Type -> Humanoid 옵션은 유니티에서 제공하는 Humanoid Type의 Bone을 사용한다는 뜻임.
- FK(Forward Kinematics, 순운동학): 어깨 -> 팔꿈치 -> 손목 순서로 부모 뼈대를 회전시켜 최종 손끝의 위치를 결정하는 방식(기본 애니메이션 방식).
- IK(Inverse Kinematics, 역운동학): 손끝이나 발끝 같은 말단부(End Effector)의 위치를 먼저 지정하면, 그에 맞춰 상위 뼈대(관절)의 각도를 자동으로 계산해 움직이게 만드는 방식.
Gun에 Player의 왼손과 오른손이 놓일 위치를 지정해놓고 IK를 통해서 Player의 팔 위치를 역산할 수 있다.
// PlayerShooter에서 왼손 위치와 오른손 위치를 통해 Player의 상체 모양을 설정하는 예시
private void OnAnimatorIK(int layerIndex)
{
gunPivot.position = playerAnimator.GetIKHintPosition(AvatarIKHint.RightElbow);
playerAnimator.SetIKPositionWeight(AvatarIKGoal.LeftHand, 1f);
playerAnimator.SetIKRotationWeight(AvatarIKGoal.LeftHand, 1f);
playerAnimator.SetIKPosition(AvatarIKGoal.LeftHand, leftHandMount.position);
playerAnimator.SetIKRotation(AvatarIKGoal.LeftHand, leftHandMount.rotation);
playerAnimator.SetIKPositionWeight(AvatarIKGoal.RightHand, 1f);
playerAnimator.SetIKRotationWeight(AvatarIKGoal.RightHand, 1f);
playerAnimator.SetIKPosition(AvatarIKGoal.RightHand, rightHandMount.position);
playerAnimator.SetIKRotation(AvatarIKGoal.RightHand, rightHandMount.rotation);
}
OnAnimatorIK()의 자동 호출: GameObject에 Animator 컴포넌트 부착 및 Animator 레이어에서 IK Pass 가 켜져있는지 확인 후OnAnimatorIK()함수를 작성하면 스크립트 상에서 개발자가 직접OnAnimatorIK()를 호출하거나 연결해 줄 필요가 없이 유니티 엔진이 내부적으로 조건을 감지하고 알아서 자동으로 실행(호출)시켜 준다.



