728x90

이 글은 인프런 "따라하면서 배우는 고박사의 유니티 기초" 강의를 수강한 후 정리한 글입니다. 

 

[지금 무료] 따라하면서 배우는 고박사의 유니티 기초 | 고박사 - 인프런

고박사 | 유니티로 게임을 개발하고 싶은 초보자를 대상으로 하며, 유니티 설치부터 2D/3D 게임 개발에 필요한 기초 지식까지 자세하게 설명합니다. (강의에 사용되는 모든 리소스는 영상 상단의

www.inflearn.com

 

이번 실습 진행을 위해서는 영상 하단에 게임 월드 패키지를 다운받고 Unity chan! 페이지(https://unity-chan.com/)에서 Data Download를 누르고 라이센스 동의한 후 아래 에셋을 다운받은 후 유니티에 임포트한다.

 

유니티 에셋 스토어에서 플레이어 캐릭터 애니메이션을 구현하기 위해 "RPG Character Mecanim Animation Pack FREE" 다운 받는다.

 

 


게임 월드 구성과 플레이어 캐릭터

Asset의 Prefab 폴더에 있는 게임월드를 hierarchy view에 드래그 한 후 main camera를 position을 설정한다.

이후 빈 오브젝트를 생성해서 player로 사용할 예정이다. 다운받은 캐릭터 에셋을 적용하기 전, model의 크기를 Scale Factor를 2로 키워 모델의 크기를 키워주고 Rig 탭 Optimize Game Object 최적화, 오른속에 무기들기때문에 Right hand 체크한다.


이것을 player의 자식으로 배치한다. 매우 중요하다!! 생성한 player 오브젝트 자식으로 배치하지 않으면 이후에 스크립트 적용시에 nullreferenceexception 오류가 발생할 수 있기 때문이다.. (실제 오류 사례...)

 

캐릭터가 핑크색이 된 이슈 해결

더보기

캐릭터가 마젠타 핑크색가 되어 버렸다..

이렇게 되는 이유는 materials의 문제가 발생한 경우로 일반적인 경우에는 Render Pipeline을 적용하면 된다고 했지만.. 2022.3.22 버전의 유니티에서는 그런 기능은 없다..

 

없다면 방법은 두 가지이다. 유니티 버전 다운그레이드를 하거나, 일일이 materials의 shader를 standard로 바꿔주면 된다. 

 

CameraController 스크립트

이전 강의 Navigation mesh 에서 작성한 스크립트랑 동일하며, main camera의 타겟 추적, 타겟과의 거리 조절, 회전을 제어하는 스크립트를 작성한다. 

using UnityEngine;

public class CameraController : MonoBehaviour
{
	[SerializeField]
	private	Transform	target;				// 카메라가 추적하는 대상
	[SerializeField]
	private	float		minDistance = 3;	// 카메라와 target의 최소 거리
	[SerializeField]
	private	float		maxDistance = 30;	// 카메라와 target의 최대 거리
	[SerializeField]
	private	float		wheelSpeed = 500;	// 마우스 휠 스크롤 속도
	[SerializeField]
	private	float		xMoveSpeed = 500;	// 카메라의 y축 회전 속도
	[SerializeField]
	private	float		yMoveSpeed = 250;	// 카메라의 x축 회전 속도
	private	float		yMinLimit = 5;		// 카메라 x축 회전 제한 최소 값
	private	float		yMaxLimit = 80;		// 카메라 x축 회전 제한 최대 값
	private	float		x, y;				// 마우스 이동 방향 값
	private	float		distance;			// 카메라와 target의 거리

	private void Awake()
	{
		// 최초 설정된 target과 카메라의 위치를 바탕으로 distance 값 초기화
		distance = Vector3.Distance(transform.position, target.position);
		// 최초 카메라의 회전 값을 x, y 변수에 저장
		Vector3 angles = transform.eulerAngles;
		x = angles.y;
		y = angles.x;
	}

	private void Update()
	{
		if ( target == null ) return;	// target이 존재하지 않으면 실행 하지 않는다

		// 오른쪽 마우스를 누르고 있을 때
		if ( Input.GetMouseButton(1) )
		{
			// 마우스를 x, y축 움직임 방향 정보
			x += Input.GetAxis("Mouse X") * xMoveSpeed * Time.deltaTime;
			y -= Input.GetAxis("Mouse Y") * yMoveSpeed * Time.deltaTime;
			// 오브젝트의 위/아래(x축) 한계 범위 설정
			y = ClampAngle(y, yMinLimit, yMaxLimit);
			// 카메라의 회전(Rotation) 정보 갱신
			transform.rotation = Quaternion.Euler(y, x, 0);
		}

		// 마우스 휠 스크롤을 이용해 target과 카메라의 거리 값(distance) 조절
		distance -= Input.GetAxis("Mouse ScrollWheel") * wheelSpeed * Time.deltaTime;
		// 거리는 최소, 최대 거리를 설정해서 그 값을 벗어나지 않도록 한다
		distance = Mathf.Clamp(distance, minDistance, maxDistance);
	}

	private void LateUpdate()
	{
		if ( target == null ) return;	// target이 존재하지 않으면 실행 하지 않는다

		// 카메라의 위치(Position) 정보 갱신
		// target의 위치를 기준으로 distacne만큼 떨어져서 쫓아간다
		transform.position = transform.rotation * new Vector3(0, 0, -distance) + target.position;
	}
	
	private float ClampAngle(float angle, float min, float max)
	{
		if ( angle < -360 )	angle += 360;
		if ( angle > 360 )	angle -= 360;

		return Mathf.Clamp(angle, min, max);
	}
}

위의 스크립트를 main camera의 component로 적용한 후 타겟 변수에는  player object의 자식으로 빈 오브젝트를 생성해서 CameraTarget으로 이름을 변경 후 위치를 조정한다. main camera의 타겟 변수에 적용한다. 


플레이어 오브젝트의 위치는 현재 모델의 발끝으로 설정되어 있어 카메라가 보는 위치 조정을 위해 빈 오브젝트를 만들어 설정하였다. 

 


플레이어 캐릭터 이동 

Movement3D & PlayerController 스크립트 

이동이 가능한 오브젝트에 이동 제어에 사용되는 Movement3D 스크립트 작성한다. 

using UnityEngine;

public class Movement3D : MonoBehaviour
{
    [SerializeField]
    private float   moveSpeed = 5;      // 이동 속도
    private Vector3 moveDirection;      // 이동 방향

    private CharacterController characterController;

    public float MoveSpeed
    {
        // 이동속도는 2~5 사이의 값만 설정 가능
        set => moveSpeed = Mathf.Clamp (value, 2.0f, 5.0f);
    }
    
    private void Awake()
    {
        characterController = GetComponent<CharacterController>();
    }
    
    private void Update()
    {
        // 이동 설정. CharacterController의 Move() 함수를 이용한 이동 
        characterController. Move(moveDirection * moveSpeed * Time.deltaTime);
    }

    public void MoveTo(Vector3 direction)
    {
        moveDirection = new Vector3(direction.x, moveDirection.y, direction.z);
    }

    public void JumpTo()
    {
        //캐릭터가 바닥을 밟고 있으면 점프
        if (characterController.isGrounded == true)
        {
            moveDirection.y = jumpForce;   
        }
    }

}

 

외부에서 사용하는 MoveTo()를 이용하여 이동방향을 설정하고, Update()에서 characterController class의 Move()를 호출하여 오브젝트가 moveDirection 방향으로 이동하게 된다. 



플레이어를 제어하는 PlayerController 스크립트를 작성한다. 

 

using UnityEngine;

public class PlayerController : MonoBehaviour
{
    [SerializeField] 
    private KeyCode     jumpKeyCode = KeyCode.Space;
    [SerializeField] 
    private Transform   cameraTransform;
    private Movement3D  movement3D;

    
    private void Awake()
    {
        Cursor.visible      = false;                   // 마우스 커서를 보이지 않게 
        Cursor.lockState    = CursorLockMode.Locked;   // 마우스 커서 위치 고정
        
        movement3D = GetComponent<Movement3D>();

    }
    private void Update()
    {
        // 방향키를 눌러 이동
        float x = Input.GetAxis("Horizontal");
        float z = Input.GetAxis("Vertical");

        // 이동 속도 설정 (앞으로 이동할때만 5, 나머지는 2)
        movement3D.MoveSpeed = z > 0? 5.0f: 2.0f;
        // 이동 함수 호출 (카메라가 보고있는 방향을 기준으로 방향키에 따라 이동)
        movement3D.MoveTo(cameraTransform.rotation * new Vector3(x, 0, z));

        // 회전 설정 (항상 앞만 보도록 캐릭터의 회전은 카메라와 같은 회전 값으로 설정)
        transform.rotation = Quaternion.Euler(0, cameraTransform.eulerAngles.y,0);
    }
}

캐릭터가 카메라가 바라보는 전방 방향을 계속 바라보도록 회전 설정을 추가하였으며 플레이어의 y축 회전값과 카메라의 y축 회전값이 동일하게 움직이고 있음을 알 수 있다.

 

player 오브젝트에 방금 작성한 스크립트 2개와 character controller도 적용한다. step offset을 0.4, center를 (0,1,0)으로 설정한다. playercController 스크립트에 Main camera를 등록한다. 

 


플레이어 캐릭터 물리와 점프

Movement3D 스크립트

 [SerializeField]
    private float   gravity = -9.81f;   // 중력 계수
    [SerializeField]
    private float   jumpForce = 3.0f;   // 점프 힘

'''

 // 중력 설정, 플레이어가 땅을 밟고 있지 않다면
        // y축 이동방향에 gravity * Time.deltaTime을 더해준다.
        if ( characterController.isGrounded == false)
        {
            moveDirection.y += gravity * Time.deltaTime;
        }

Movement3D 스크립트에 중력 계수와 뛰어오르는 힘을 변수로 설정하고, 플레이어가 공중에 떠 있을 때 moveDirection.y += gravity * Time.deltaTime을 해준다. 

 

PlayerController 스크립트 

외부에서 호출하는 JumpTo 함수를 통해 플레이어가 바닥을 밟고 있으면 y축 이동 방향에 jumpForce 값을 설정하여 jump를 하게 된다. jump키 입력과 JumpTo함수 참조하도록 수정한다. 

 if (Input.GetKeyDown(jumpKeyCode))
        {
            movement3D.JumpTo();
        }

 


플레이어 캐릭터 대기, 이동, 점프 애니메이션

 

애니메이션 적용 전 player에게 무기를 들려주겠다. 애니메이션 손동작에 맞추어 무기가 함께 움직여야 하기 때문에 캐릭터의 오른손 레퍼런스인 righthand object의 자식으로 sword asset을 설정하였다. 

무기의 Transform 정보를 수정한 뒤 material을 적용한다. 

 

Marie_sum Animator에 Player( Animator Cotroller )를 적용하고, Apply Root Motion을 해제한다. 

 

Marie_sum의 Animator view로 blend tree를 생성한 뒤 Movement로 설정한다. blend tree 내부로 이동하여 parameter를 float type 으로 horizontal와 vertical로 설정한다.


blend tree의 type을 2D Simple Directional로 설정하고 파라미터의 Pos x,Pos y축에도 아래 이미지와 같이 설정한다. 

5개의 motion을 만들어주고, 각 모션을 위처럼 설정한다. 

이후에 상태전이를 생성해서 movement에서 jump로 전이하는 조건 설정을 위해 트리거 타입의 파라미터 onJump를 생성한다. 그리고 상태 전이에서 Has Exit Time을 해제한다. 


스토어에서 받아온 RPG 캐릭터 에니메이션에는 이미 이벤트 함수가 설정되어 있기 때문에, 실습에서 사용하는 애니메이션 클립의 이번트에 가서 이벤트 함수를 삭제한다. 

events 탭에 delete event를 해주고 apply 해주면 된다. 

애니메이션 strafe-backward, strafe-right, strafe-left, attack-kick, attack1,2,3,4 모두 적용한다. 


PlayerAnimator

애니메이션 관리, 제어를 하는 PlayerAnimator 스크립트를 작성한다. 

using UnityEngine;

public class PlayerAnimator : MonoBehaviour
{
    [SerializeField]
    private GameObject attackCollision;
    private Animator    animator;
    
    private void Awake()
    {
        animator = GetComponent<Animator>();
    }
    
    public void OnMovement (float horizontal, float vertical)
    {
        animator.SetFloat("horizontal", horizontal);
        animator.SetFloat("vertical", vertical);
    }

    public void OnJump()
    {
        animator.SetTrigger("onjump");
    }

}

위의 스크립트는 애니메이터 파라미터를 제어하는 함수들이 정의되어 있고, OnMovement()는 대기와 이동의 애니메이션 재생에 사용되는 Horizontal, Vertical 파라미터를 제어하고, OnJump()는 애니메이션 재생에 사용되는 onJump 파라미터를 제어한다. 

PlayerController 스크립트 추가

대기, 이동, 점프 행동에 맞춰 애니메이션을 제어하기 위해 PlayerController 스크립트를 수정한다. 

    private PlayerAnimator playerAnimator;
    playerAnimator = GetComponentInChildren<PlayerAnimator>();
'''
  private void Update()
    {
        // 방향키를 눌러 이동
        float x = Input.GetAxis("Horizontal");
        float z = Input.GetAxis("Vertical");
        
        // 애니메이션 파라미터 설정 (horizontal, verticla)
        playerAnimator.OnMovement(x,z);
'''

      if (Input.GetKeyDown(jumpKeyCode))
            {
                playerAnimator.OnJump();
                movement3D.JumpTo();
            }
'''
}

 

playerAnimator 변수를 만들어 자식에 있는 컴포넌트를 받아오기 위해 GetComponentInChildren을 사용한다. playerAnimator.OnMovement(x,z)매개변수로 방향값을 사용해 대기, 이동 애니메이션을 제어한다. OnJump()를 스페이스 바를 누르면 호출하여, 점프 애니메이션이 되는 것을 알 수 있다. 

위의 playerAnimator 스크립트를 컴포넌트에 적용하고, 게임을 실행하면 대기, 이동, 점프 시에 각각의 애니메이션이 적용되는 것을 알 수 있다. 


플레이어 캐릭터 공격 애니메이션

 

이번에는 발차기를 이용한 공격과 무기를 이용한 연계 공격을 실습하고자 한다.

캐릭터의 animator view로 가서 원하는 공격 애니메이션을 드래그한다. 강의의 경우에서 사용한 2Hand-Sword-Attack-Kick-R1이 없기 때문에 애니메이션 중에서 Unarmed-Attack-R1을 넣어주었다.  


 

전이설정을 위해 trigger 타입의 파라미터 OnKickAttack도 생성하였다.


공격 콤보

animator view에 sub-state Machine을 하나 생성하고 이것의 이름을 WeaponComboAttack으로 설정한다. sub-state Machine의 내부로 이동해서 무기를 이용한 공격 애니메이션을 등록한다. 

 

연계 공격이 가능하도록 attack끼리 상태전이를 연결해준다.. entry에서 연결되는 첫 공격인 attack3으로 첫 공격이 되고, attack1,2,4를 연결해준다. states > movement로 연결해준다. 

공격 전이 조건 설정을 위해 trigger type의 파라미터 onWeaponAttack를 생성한다.

 

공격에서 공격으로 이어지는 상태전이를 선택하고 Condition에 해당 파라미터를 등록한다. 이때 Has Exit Time은 true로 둔다. 공격이 완전히 끝난 이후에 다음 공격을 할 수 있도록 하기 위함이다. 


모든 공격에서 Base Layer로 들어가는 상태전이는 Exit Time은 0.9, Transition Duration은 0.1로 설정한다.

 

Base Layer로 돌아가 Movement에서 WeaponComboAttack으로 이어지는 트랜젝션을 생성한다.

 

해당 트렌젝션은 원할 때 바로 갈 수 있도록 Has Exit Time을 풀어준다. Condition에 OnWeaponAttack을 등록한다. 


플레이어의 물리적 공격과 적 피격 

마우스 좌,우클릭을 하면  공격 애니메이션 재생이 되며 특정 프레임에 이벤트 함수 호출된다. 이때 충돌 박스 AttackCollision 오브젝트 활성화되어서 공격 충돌 박스에 오브젝트가 부딪히게 되면 Take Damage() 함수 호출하면서 피격 애니메이션, 오브젝트 색상 변경이 적용된다. 



Player의 공격을 받게 될 Enemy 오브젝트를 생성하고, Enemy AnimationController 생성하여,Enemy 오브젝트에 적용 후 Apply Root Motion을 해제한다. Capsule Collider 컴포넌트를 추가 후 범위를 지정한다. 


이후 Enemy 오브젝트에 Meterial를 적용한다. 

 

해당 캐릭터는 가만히 서있는 대기 동작과 공격당했을 때의 피격 동작을 수행한다. 피격은 어떤 상황에서도 발생할 수 있기에 Any State를 활용해서 연결되는 상태전이는 모든 상태에서 전이가 가능하다.

​Any state에서 전이하는 조건 설정을 위해 trriger 파라미터 onHit를 생성해서 Condition에 설정한다.  Has Exit Time을 해제한다.


EnemyController 스크립트

캐릭터가 공격받았을 때 애니메이션 재생 등의 처리를 하는 EnemyController 스크립트를 작성한다. 

using System.Collections;
using UnityEngine;

public class EnemyController : MonoBehaviour
{
    private Animator            animator;
    private SkinnedMeshRenderer meshRenderer;
    private Color               originColor;

    private void Awake()
    {
        animator     = GetComponent<Animator>();
        meshRenderer = GetComponentInChildren<SkinnedMeshRenderer>();
        originColor  = meshRenderer.material.color;
    }

    public void TakeDamage(int damage)
    {
        // 체력이 감소되거나 피격 애니메이션이 재생되는 등의 코드를 작성 
        Debug.Log(damage+"의 체력이 감소합니다");
        // 피격 애니메이션 재생
        animator.SetTrigger("onhit");
        // 색상 변경
        StartCoroutine("OnHitColor");
    }

    private IEnumerator OnHitColor()
    {
        // 색을 빨간색으로 변경한 후 0.1초 후에 원래 색상으로 변경
        meshRenderer.material.color = Color.red;
        yield return new WaitForSeconds (0.1f);
        meshRenderer.material.color = originColor;
    }
}

 

위의 스크립트를 Enemy 오브젝트에 등록한다. player 자식오브젝트 AttackCollision을 생성해서 위치와 크기를 설정한 뒤 모습이 안보이게 mesh 해제, Box Collider, Rigidbody 추가하고 각각 is Trigger 체크, Use Gravity 해제 설정을 해준다.

 

PlayerAttackCollision 스크립트

공격 충돌 박스를 제어하는 PlayerAttackCollision 스크립트를 작성한다. 

using System.Collections;
using UnityEngine;

public class PlayerAttackCollision : MonoBehaviour
{
    private void OnEnable()
    {
        StartCoroutine("AutoDisable");
    }
    private void OnTriggerEnter (Collider other)
    {
        // 플레이어가 타격하는 대상의 태그, 컴포넌트, 함수는 바뀔 수 있다
        if (other.CompareTag("Enemy"))
        {
            other.GetComponent<EnemyController>().TakeDamage(10);
        }
    }
    private IEnumerator AutoDisable()
    {
        // 0.1초 후에 오브젝트가 사라지도록 한다
        yield return new WaitForSeconds(0.1f);
        gameObject.SetActive(false);
    }
}

충돌박스가 활성화되면 0.1초 뒤에 박스가 사라지도록 한다. 충돌 태그가 enemy 이면 TakeDamage 충돌박스에 부딪친 적의 체력을 감소시킨다.


태그는 위와 같이 설정해 줄 수 있다. 

 

PlayerAnimator 스크립트 추가

using UnityEngine;

public class PlayerAnimator : MonoBehaviour
{
    [SerializeField]
    private GameObject attackCollision;
    private Animator animator;

    private void Awake()
    {
        animator = GetComponent<Animator>();
    }

    public void OnMovement(float horizontal, float vertical)
    {
        animator.SetFloat("horizontal", horizontal);
        animator.SetFloat("vertical", vertical);
    }

    public void OnJump()
    {
        animator.SetTrigger("onJump");
    }

    public void OnKickAttack()
    {
        animator.SetTrigger("onKickAttack");
    }
    public void OnWeaponAttack()
    {
        animator.SetTrigger("onWeaponAttack");
    }
    public void OnAttackCollision()
    {
        attackCollision.SetActive(true);
    }
}

특정 프레임에서 호출하도록 스크립트를 설정하였다. 이후 Marie_sum 캐릭터 오브젝트에 등록된 playerAnimator 스크립트에 AttackCollision에 생성한 충돌박스를 넣어준다. 

 

현재 사용하고 있는 공격 animation에 가서 event에 원하는 프레임을 설정하고 add event를 이용해 함수를 생성한다. 호출을 원하는 함수를 등록한다.

 

 

게임을 재생하면 동작을 수행하게 된다. 이동, 점프, 공격 모두 정상적으로 되는데 왜 무한 루프를 하는걸까.. 알 수 없다. 

+ Recent posts