충돌을 처리하기 위해서는 Collider와 Rigidbody가 필요하다.

Collider는 충돌을 감지하는 기능을 한다. 모양에 따라 Box, Sphere, Mesh 등 여러가지 형태를 가지고 있다. Rigidbody는 물리적인 법칙을 적용하기 위해 달아주어야한다. Rigidbody는 최소한의 사용을 지향하는 것이 좋기 때문에 주로 변화가 많은 캐릭터에 주는 편이다.

Rigidbody는 Rigidbody가 달려있는 물체가 다른 Collider가 달려있는 물체와 충돌했을 때 충돌에 대해 소통할 수 있게 해준다.

충돌이 발생하면 유니티에서는 OnCollisionEnter, OnCollisionStay, OnCollisionExit 등의 이벤트를 발생시킨다.

 

타일맵(Tilemap)

이미지들을 바둑판 배열 방식으로 맵을 구성하는 것.

 

Hierarchy에 우클릭해서 2D Object - Tilemap에서 Rectangular로 만들어준다.

 

Window - 2D - Tile Palette에서 Create New Pallete를 해준 뒤, 타일 이미지 Assets을 추가한다.

 

적당히 꾸며주고 Collision을 따로 만들어 Tilemap Collider 2D를 추가해준 후, 충돌범위를 지정해주고 투명도를 조절해서 보이지 않게 해준다.

Color에서 투명도 조절

 

플레이어에 Box Collider 2D를 Component 하여 방금 만들어둔 벽면 밖으로 나가지 못하도록 한다.

 

 

조준(Aim) 시스템

무기 Asset을 유니티에 끌어온 뒤, 스크립트를 작성해준다. flipY는 Y축을 기준으로 뒤집는 코드로, 캐릭터를 기준으로 무기가 90도를 넘어가면 뒤집도록 한다. 즉, 마우스 방향으로 캐릭터를 뒤집어준다.

[SerializeField] private SpriteRenderer armRenderer;
[SerializeField] private Transform armPivot;

[SerializeField] private SpriteRenderer characterRenderer;

private TopDownCharacterController _controller; // 캐릭터가 바라보도록 한다

private void Awake()
{
    _controller = GetComponent<TopDownCharacterController>();
}

void Start()
{
    _controller.OnLookEvent += OnAim;
}

public void OnAim(Vector2 newAimDirection)
{
    RotateArm(newAimDirection);
}

private void RotateArm(Vector2 direction)
{
	float rotZ = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg;
    
    armRenderer.flipY = Mathf.Abs(rotZ) > 90f; // y축을 기준으로 뒤집는 코드
    characterRenderer.flipX = armRenderer.flipY; // 캐릭터를 기준으로 무기가 90도를 넘어가게 되면 뒤집는다
    armPivot.rotation = Quaternion.Euler(0, 0, rotZ); // 무기 회전
}

 

rotation이라고 하는 회전 값은 Quaternion이라고 하는 4원소 값을 쓰고 있기 때문에 우리가 일반적으로 사용하기 쉽지 않다. 아크 탄젠트로 세타 값이 나오면 Pi값, 즉 라디안 값이 나온다. 그러므로 라디안 값을 오일러(Euler) 값인 degree(도)로 바꾸는 값을 곱해준다.

 

Atan2(x, y)는 아크 탄젠트(역탄젠트)를 구하는 것이다. x와 y 값을 구해서 아크 탄젠트를 구하면 세타( θ)이 나온다. 즉, 벡터의 각도를 구하는 것이다.

 

캐릭터에 스크립트를 Component 하여 각각 지정해준다.

 

 

공격 시스템

공격을 구현하기 위한 스크립트를 생성한다.

보통 Awake에서 Component들을 준비하고 Start나 이후에 그 코드를 활용하는 식으로 구성한다.

 

일단 CharacterController 스크립트에서 Attack을 처리를 추가해준다.

public class TopDownCharacterController : MonoBehaviour
{
    // event : 외부에서 호출하지 못하게 막아주는 기능
    public event Action<Vector2> OnMoveEvent;
    public event Action<Vector2> OnLookEvent;
    public event Action OnAttackEvent;

    private float _timeSinceLastAttack = float.MaxValue; // 마지막으로 공격한 시간
    protected bool IsAttacking { get; set; } // Attack에 대한 프로퍼티

    protected virtual void Update() // virtual: 하위에서 상속받아 오버라이드에서 쓸 수 있도록 함
    {
        HandleAttackDelay();
    }

    private void HandleAttackDelay() // 공격에 대한 시스템 구현만
    {
        if (_timeSinceLastAttack <= 0.2f) // 나중에 수정
        {
            _timeSinceLastAttack += Time.deltaTime;
        }
        
        if (IsAttacking && _timeSinceLastAttack > 0.2f)
        {
            _timeSinceLastAttack = 0;
            CallAttackEvent();
        }
    }

    public void CallMoveEvent(Vector2 direction)
    {
        OnMoveEvent?.Invoke(direction); //?. : OnLookEvent이 Null이 아닐 때만 동작
    }

    public void CallLookEvent(Vector2 direction)
    {
        OnLookEvent?.Invoke(direction);
    }

    public void CallAttackEvent()
    {
        OnAttackEvent?.Invoke();
    }
}

 

 

CharacterInputController에 OnFire 키 입력값을 받으면 CharacterController의 HandleAttackDelay() 공격을 실행한다.

 private void OnFire(InputValue value)
 {
     IsAttacking = value.isPressed;
 }

 

이제 아까 만들어둔 실제 공격하는 스크립트를 작성한다.

public class TopDown : MonoBehaviour
{
    private TopDownCharacterController _controller;

    [SerializeField] private Transform projectileSpawnPosition;
    private Vector2 _aimDirection = Vector2.right;

    private void Awake()
    {
        _controller = GetComponent<TopDownCharacterController>();
    }

    // Start is called before the first frame update
    void Start()
    {
        _controller.OnAttackEvent += OnShoot;
        _controller.OnLookEvent += OnAim;
    }

    private void OnAim(Vector2 newAimDirection) // 에임 위치를 잡아준다
    {
        _aimDirection = newAimDirection;
    }

    private void OnShoot() // 실제 공격
    {
        CreateProjectile();
    }

    private void CreateProjectile()
    {
        Debug.Log("Fire");
    }
}


유니티에서 Prefabs 파일을 만든 뒤, 발사체 GameObject를 만들어서 파일에 넣어준다.

Prefab은 유니티에서 오브젝트들을 한 번 만들어놓고 재사용하여 관리하기 쉽게 만들어진 것으로 미리 설정된 값을 그대로 쓸 수 있다. 현재 복제가 되어 나왔기 때문에 원본과는 별개로 동작할 수 있다. 모든 인스턴스를 일괄적으로 업데이트 할 수 있고 다양한 바리에이션을 만들 수 있기 때문에 Prefab으로 대부분의 오브젝트를 만든다.

 

Player에서 임시로 발사체를 받은 뒤, 복사체가 잘 생성되나 테스트하기 위해 TopDownShooting 스크립트에 아래 코드를 추가한다.

public GameObject testPrefab; // 임시 코드

 private void CreateProjectile()
 {
     Instantiate(testPrefab, projectileSpawnPosition.position, Quaternion.identity);
 }

Instantiate()는 동적 생성으로 원본을 받은 것을 복제본으로 만든다. 그런데 현재 원본의 형태가 유니티 오브젝트 형식이다. 오브젝트 형식은 유니티에서 사용하는 대부분의 오브젝트를 최상위로 가지고 있기 때문에 Asset, Material 등 모두 사용할 수 있다.

 

 

+ Recent posts