[Unity] Knife Hit (2)

suhan0304·2024년 8월 22일

유니티 - Knife Hit

목록 보기
2/9
post-thumbnail

Update Knife

칼끼리의 충돌 관리를 해주려고 했는데 문제점을 하나 발견했다.

칼이 Target에 박힌 이후에 칼들끼리 충돌이 발생한다. 물론 예외처리로 이미 꽂힌 칼들끼리는 충돌을 감지해도 로직이 실행되지 않도록 할 수는 있는데 그러면 결국 충돌 이벤트를 계속해서 실행한다는 뜻이라 성능적으로 비효율적이다. 그래서 Collider의 모양을 아래와 같이 바꿔주었다.

Unity에서 Circle이나 Box처럼 삼각형 Collider를 바로 지원해주지는 않는다. Polygon Colldier 2D를 써서 자체적으로 Edit Collider로 원하는 Collider 모양을 만들 수 있다. 웬만하면 변의 개수를 최소화 시킨 다각형으로 만드는 게 좋다. 아래처럼 변이 많아질 수록 Polygon 개수가 늘어나서 처리량이 늘어난다.

이렇게 삼각형으로 바꾼 다음에 확인해보니 꽂힌 칼들끼리의 충돌이 일어나지 않는다.

칼 스프라이트를 임포트 하면서 쓸만한 다른 이미지도 몇개 임포트 해줬다.

좀 괜찮아 보이는 것 같다!


Remain Knives UI

먼저 남은 칼의 개수 변수를 만들어서 OnHit마다 감소되도록 해서 0이 되면 파괴되도록 하자.

파괴는 잘 된다. 남은 칼 개수를 표시할 UI를 제작해보자.

그럴싸 해 보인다. 이제 스크립트로 Target의 체력만큼 Icon을 만들도록 하고 화면을 터치할 때 마다 Icon의 Color를 바꾸도록 스크립트를 작성해보자. Target 프리팹이 자체적으로 자신이 파괴되는데 필요한 knife 개수를 변수로 가지고 있고 GameManager에서 Target을 스폰할 때 해당 변수 값을 읽어서 UI Manager로 넘겨준다.

GameManager.cs

[Button("SpawnTarget")]
public void SpawnTarget() {
    GameObject currentTarget = Instantiate(target);
    RemainKnives = currentTarget.GetComponent<Target>().knivesToDestroy;
    UIManager.Instance.SpawnKnivesIcon(RemainKnives);
}

knife 개수를 매개벼수로 넘겨받아 아이콘을 생성하도록 한다.

UIManager.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class UIManager : MonoBehaviour
{
    public static UIManager Instance;

    public GameObject knifeIconsContainer;
    public GameObject KnifeIconPrefab;
    public List<GameObject> KnifeIcons;

    private void OnEnable() {
        if (Instance == null) {
            Instance = this;
        }
        else {
            Destroy(gameObject);
        }
    }

    public void SpawnKnivesIcon(int cntKnives) {
        for (int i = 0; i < cntKnives; i++) {
            GameObject knifeIcon = Instantiate(KnifeIconPrefab);
            knifeIcon.transform.SetParent(knifeIconsContainer.transform);
            KnifeIcons.Add(knifeIcon);
        }
    }
}

이제 OnTouchScreen 마다 knife 개수를 줄이고 아이콘에도 적용시켜준다. 이 때 RemainKnives가 0이면 Target을 파괴시켜주도록 한다.

GameManager.cs

public void OnTouchScreen() {
    RemainKnives--;
    UIManager.Instance.DecreaseKnifeCount(RemainKnives);
    if (RemainKnives == 0) {
        Events.OnAllKnivesOnHit.Invoke();
    }
}

UIManager.cs

public void DecreaseKnifeCount(int RemainKnives) {
    if (RemainKnives < 0) {
        return; 
    }
    KnifeIcons[(KnifeIcons.Count - 1)- RemainKnives].GetComponent<Image>().color = Color.black;
}

Target.cs

[Button("DestroyTarget")]
public void DestroyTarget() {
    transform.DOKill();
    //TODO - Destroy(gameObject);
}

일단 잘 생성되서 icon이 배치되고

정상적으로 터치 할 때마다 칼 icon도 잘 변경된다.

문제가 좀 있었는데 이게 OnHit일때 RemainKnives를 체크하도록 수정해서 고쳤다.

[Button("OnHit")]
public void OnHit() {
    transform.DOPunchPosition(Vector3.right * shakeStrength, shakeDuration, vibrato, randomness);
    StartCoroutine(FlashCoroutine());
    if (GameManager.Instance.RemainKnives == 0) {
        Events.OnAllKnivesOnHit.Invoke();
    }
}

collision between Knives

이제 칼 간의 충돌을 구현하고 게임 오버만 구현하면 일단 MainScene에서의 작업은 마무리 할 수 있다. 일단 GameOver를 구현해보자. 실제 인 게임 영상을 보면 화면이 반짝이면서 칼이 튕겨나가고 게임 오버가 된다.

칼이 튕겨 나가서 떨어지도록 하려면 다시 중력의 영향을 받아야하고, 좀 회전하듯이 튕겨나가야 해서 힘도 랜덤하게 주어야한다.

else if(collision.gameObject.CompareTag("Knife") && canMove) {
    canMove = false;
    Events.OnTouchScreen -= FireKnife;   

    Events.OnCollisionBetweenKnives.Invoke();
    rb.velocity = Vector3.zero;

    rb.isKinematic = false;

    Vector2 collisionDirection = (transform.position - collision.transform.position).normalized;
    float bounceForce = Random.Range(5f, 7.5f);

    float randomAngle = Random.Range(-60f, 60f);
    Vector2 forceDirection = Quaternion.Euler(0, 0, randomAngle) * collisionDirection;

    rb.AddForce(forceDirection * bounceForce, ForceMode2D.Impulse);

    float randomTorque = Random.Range(-200f, 200f);
    rb.angularVelocity = randomTorque;
}

좀 복잡해보이지만 한줄씩 따라가면 쉽다.

  • 충돌 지점으로 부터 수직 벡터, 즉 충돌 한 방향과 반대방향을 구하기 위해 Collision의 transform을 사용해 충돌 벡터(단위 벡터)를 구한다.
  • 충돌 벡터를 구했으면 그대로 거울처럼 튕겨나는게 아니라, 랜덤한 힘으로, 랜덤한 방향성으로 AddForce로 힘을 받아서 튕겨나도록 한다.
  • 이때 칼이 단순히 튕겨나는게 아니라 회전하면서 튕겨나는게 좋을 것 같다. auglarVelocity에 랜덤한 값을 적용시켜준다.

잘 튕겨난다. 화면 반짝임은 화면 전체를 덮는 오브젝트를 하나 만들어서 그냥 DOColor로 반짝이는 효과를 줘보자.

화면을 모두 덮는 Square 오브젝트를 흰색으로 하나 만들어주고 아래와 같이 작성해줬다.

using System.Collections;
using System.Collections.Generic;
using DG.Tweening;
using Sirenix.OdinInspector;
using UnityEngine;

public class BackgroundEffect : MonoBehaviour
{
    public GameObject Background_White;

    private SpriteRenderer spriteRenderer;

    public float flashDuration = 0.5f;
    public float flashStrength = 0.25f;

    private void Start() {
        spriteRenderer = Background_White.GetComponent<SpriteRenderer>();
    }

    private void OnEnable() {
        Events.OnCollisionBetweenKnives += BackgroundFlash;
    }

    private void OnDisable() {
        Events.OnCollisionBetweenKnives -= BackgroundFlash;
    }

    [Button("Flash")]
    public void BackgroundFlash() {
        spriteRenderer.DOFade(flashStrength, flashDuration / 3)
            .OnComplete(() => spriteRenderer.DOFade(0f, flashDuration / 3 * 2));
    }

}

이래서 Events로 구현하는게 정말 좋다. 이렇게 구독만 해주면 바로바로 이벤트 발생때마다 실행되도록 구현할 수 있다.

화면이 잘 깜빡인다. 그리고 실제 인게임처럼 원형 이펙트가 생기도록 하고 싶은데, 그냥 이펙트 이미지를 하나 찾아서 투명도, 위치 조절로 해도 되지만 스크립트가 그러면 괜히 복잡해질 거 같아서 Particle System으로도 쉽게 구현했다. (Particle System은 위치만 잘 잡아주고 Play만 시켜주면 된다.)

충돌 위치를 잡아주려 했으나..

OnTriggerEnter2DOnCollisionEnter2D와 달리 충돌 지점을 바로 얻어올 수가 없다..

OnCollision은 아래와 같이 매우 쉽게 충돌 지점을 찾을 수 있다..

private void OnCollisionEnter2D(Collision2D collision)
{
    // 충돌한 모든 접촉점(contact point)을 가져옴
    foreach (ContactPoint2D contact in collision.contacts)
    {
        Vector2 collisionPoint = contact.point;
        Debug.Log("Collision Point: " + collisionPoint);
    }
}

결국 충돌 지점을 추정해야하는데, 두 Collider의 중심을 찾아서 평균 값을 내거나

Vector3 Position1 = this.GetComponent<Collider2D>().bounds.center;
Vector3 Position2 = collision.bound.center;

Vector3 hitPosition = ( Position1 + Position2 ) / 2

아니면 아래와 같이 작성해서 구할 수 있다.

Vector2 collisionPoint = collision.bounds.ClosestPoint(transform.position);

collision.bounds.ClosestPoint는 현재 Collider2D의 경계(bound)에서 주어진 위치(transform.position)에 가장 가까운 지점을 반환한다.

후자의 방법이 더 간단하고 코드도 직관적이라서 후자의 방법으로 구현했다.

else if(collision.gameObject.CompareTag("Knife") && canMove) {
	/* 기존 코드 */

    Vector2 collisionPoint = collision.bounds.ClosestPoint(transform.position);
    SFXManager.Instance.playImpact(collisionPoint);
}

SFXManager.cs

using System.Collections;
using System.Collections.Generic;
using DG.Tweening;
using Sirenix.OdinInspector;
using UnityEngine;

public class BackgroundEffect : MonoBehaviour
{
    public GameObject Background_White;

    private SpriteRenderer spriteRenderer;

    public float flashDuration = 0.15f;
    public float flashStrength = 0.05f;

    private void Start() {
        spriteRenderer = Background_White.GetComponent<SpriteRenderer>();
    }

    private void OnEnable() {
        Events.OnCollisionBetweenKnives += BackgroundFlash;
    }

    private void OnDisable() {
        Events.OnCollisionBetweenKnives -= BackgroundFlash;
    }

    [Button("Flash")]
    public void BackgroundFlash() {
        spriteRenderer.DOFade(flashStrength, flashDuration / 2)
            .OnComplete(() => spriteRenderer.DOFade(0f, flashDuration / 2));
    }
} 

최종적으로 아래와 같이 구현됐다.


추후 계획

아래 정도만 구현하면 그래도 대부분의 기능은 거의 구현한 듯?

  • 점수 시스템
  • 게임 오버
  • 타이틀 씬?
  • 사과 만드는거 좋을것 같기도?
profile
Be Honest, Be Harder, Be Stronger

0개의 댓글