본문 바로가기

유니티(공격 판정 / 체력바 / 아이템 드롭) _ 멋쟁이사자처럼 유니티 부트캠프 후기 33회차

@salmu2025. 7. 5. 21:30
반응형

[33회차 수업내용] 250702

1. 공격판정

2. 체력바

3. 아이템 획득

 

* (수정된 부분 정리) MonsterCore cs 

using UnityEngine.UI; // 체력바 UI 조작을 위해 Image 타입을 사용

public abstract class MonsterCore : MonoBehaviour, IDamageable
// IDamageable 인터페이스를 구현하여 데미지 처리 가능하도록 함

public ItemManager itemManager;  // 몬스터가 죽을 때 아이템을 드롭하기 위한 매니저

public Image hpBar; // 체력바 UI와 연동되는 이미지 컴포넌트

public float currHp; // 현재 체력 (hp는 최대 체력)
public float atkDamage; // 몬스터 공격력

private bool isDead; // 사망 여부 확인용 플래그. 사망 시 중복 처리 방지

// Init 메서드 확장 (공격력, 체력, 체력바, 아이템 매니저 설정 포함)
protected virtual void Init(float hp, float speed, float attackTime, float atkDamage)
{
    this.hp = hp;
    this.speed = speed;
    this.attackTime = attackTime;
    this.atkDamage = atkDamage;

    itemManager = FindFirstObjectByType<ItemManager>();

    target = GameObject.FindGameObjectWithTag("Player").transform;

    animator = GetComponent<Animator>();
    monsterRb = GetComponent<Rigidbody2D>();
    monsterColl = GetComponent<Collider2D>();

    currHp = hp;
    hpBar.fillAmount = currHp / hp; // 체력바 초기값 설정
}

// 사망한 경우 행동하지 않도록 처리
void Update()
{
    if (isDead)
        return;

    switch (monsterState)
    {
        case MonsterState.IDLE:
            Idle();
            break;
        case MonsterState.PATROL:
            Patrol();
            break;
        case MonsterState.TRACE:
            Trace();
            break;
        case MonsterState.ATTACK:
            Attack();
            break;
    }
}

// 트리거 충돌 처리
private void OnTriggerEnter2D(Collider2D other)
{
    if (other.CompareTag("Return"))
    {
        moveDir *= -1;
        transform.localScale = new Vector3(moveDir, 1, 1);
    }

    if (other.GetComponent<IDamageable>() != null)
    {
        other.GetComponent<IDamageable>().TakeDamage(atkDamage);
        // 데미지를 줄 수 있는 대상이면 공격력만큼 피해를 입힘
    }
}

// IDamageable 구현체: 외부에서 데미지를 받을 때 호출됨
public void TakeDamage(float damage)
{
    currHp -= damage;
    hpBar.fillAmount = currHp / hp; // 체력바 UI 갱신

    if (currHp <= 0f)
        Death();
}

// 사망 처리 함수
public void Death()
{
    isDead = true;
    animator.SetTrigger("Death"); // 사망 애니메이션 실행
    monsterColl.enabled = false;  // 충돌 비활성화
    monsterRb.gravityScale = 0f;  // 중력 제거 (낙하 방지)
    itemManager.DropItem(transform.position); // 아이템 드롭 실행
}

 

 

*Goblin (수정된것만)

// 공격력까지 포함하는 Init 호출로 변경
void Start()
{
    Init(30f, 3f, 2f, 10f); // 체력, 속도, 공격 쿨타임, 공격력
     // 플레이어 위치를 주기적으로 확인해 추적/공격 상태 전환
     // <기존 monsterCore Update가 아닌 코루틴으로 작성해 idle, patrol일때만 작동하게 바꿈. 제어하기 더 편함
    StartCoroutine(FindPlayerRoutine());
}

// Init 오버라이드 // 부모의 init 실행해서 컴포넌트연결, 기본값 설정 등 하고 체력바 초기화 등 진행)
protected override void Init(float hp, float speed, float attackTime, float atkDamage)
{
   // base = 부모클래스 가리킴 Goblin이 부모인 MonsterCore의 초기화 코드를 그대로 활용
    base.Init(hp, speed, attackTime, atkDamage);
    // Goblin에서 따로 설정 - 처음 대기 상태 시간 랜덤 설정 (1~5초)
    idleTime = Random.Range(1f, 5f);
}

// 플레이어와의 거리 및 방향을 판단하여 상태를 추적(TRACING) 또는 공격(ATTACK)으로 바꾸는 루프 (기존 Update 대신 동작)
IEnumerator FindPlayerRoutine()
{
    while (true) // 무한 루프. 몬스터가 살아 있는 동안(destroy되거나 비활성화되지않는한) 계속 실행됨
    {
        yield return null; // 매 프레임마다 반복
        targetDist = Vector3.Distance(transform.position, target.position);

  // 현재 상태가 IDLE 또는 PATROL일 때만 추적 조건 검사
        if (monsterState == MonsterState.IDLE || monsterState == MonsterState.PATROL)
        {
            Vector3 monsterDir = Vector3.right * moveDir;
            Vector3 playerDir = (transform.position - target.position).normalized;
            float dotValue = Vector3.Dot(monsterDir, playerDir);
            isTrace = dotValue < -0.5f && dotValue >= -1f;

            if (targetDist <= traceDist && isTrace)
            {
                animator.SetBool("isRun", true);
                ChangeState(MonsterState.TRACE);
            }
        }
        //추적중일때
        else if (monsterState == MonsterState.TRACE)
        {
            if (targetDist > traceDist)
            {
                timer = 0f;
                idleTime = Random.Range(1f, 5f);
                animator.SetBool("isRun", false); // 애니메이션 트리거도 꼭 처리
                ChangeState(MonsterState.IDLE);
            }

            if (targetDist < attackDist)
            {
                ChangeState(MonsterState.ATTACK);
            }
        }
    }
}

  public override void Idle()
    {
        timer += Time.deltaTime;
        if (timer >= idleTime)
        {
            timer = 0f;
            moveDir = Random.Range(0, 2) == 1 ? 1 : -1;
            transform.localScale = new Vector3(moveDir, 1, 1);
            //체력바는 반전되면 안되니까 다시한번 반전
            hpBar.transform.localScale = new Vector3(moveDir, 1, 1);
            
            patrolTime = Random.Range(1f, 5f);
            animator.SetBool("isRun", true);
            
            ChangeState(MonsterState.PATROL);
        }
        
         public override void Trace()
    {
        var targetDir = (target.position - transform.position).normalized;
        transform.position += Vector3.right * targetDir.x * speed * Time.deltaTime;

        var scaleX = targetDir.x > 0 ? 1 : -1;
        transform.localScale = new Vector3(scaleX, 1, 1);
         //체력바는 반전되면 안되니까 다시한번 반전
        hpBar.transform.localScale = new Vector3(scaleX, 1, 1);
    }

    public override void Attack()
    {
        if (!isAttack)
            StartCoroutine(AttackRoutine());
    }

 공격 애니메이션 시간 자동 대기 및 방향 유지, 추적 복귀
IEnumerator AttackRoutine()
{
    isAttack = true;
    animator.SetTrigger("Attack");

// 현재 애니메이터가 재생 중인 애니메이션의 길이(초)를 가져옴
    float currAnimLength = animator.GetCurrentAnimatorStateInfo(0).length;
       // 애니메이션이 끝날 때까지 대기 (시간 딜레이 삽입)
    yield return new WaitForSeconds(currAnimLength); 

// 공격 애니메이션 종료 후 달리기 애니메이션 비활성화
    animator.SetBool("isRun", false);



// 남은 공격 쿨타임 동안 대기 (총 공격 시간 - 애니메이션 시간)
//공격 시간(attackTime)을 “전체 쿨타임”으로 사용하고, 애니메이션 시간은 그보다 짧다는 전제하에 작성된 구조
    yield return new WaitForSeconds(attackTime - 1f);
    
       var targetDir = (target.position - transform.position).normalized;
    var scaleX = targetDir.x > 0 ? 1 : -1;
    transform.localScale = new Vector3(scaleX, 1, 1);
    hpBar.transform.localScale = new Vector3(scaleX, 1, 1);
    
    // 상태를 추적 상태(TRACE)로 전환하여 플레이어 쫓도록 함
    isAttack = false;
    animator.SetBool("isRun", true);
    ChangeState(MonsterState.TRACE);
}
  • yield return null  “다음 프레임까지 대기”
    • Unity는 매 프레임마다 코루틴을 업데이트하면서 yield return으로 멈췄던 지점에서 다시 이어서 실행함.
    • 따라서 while (true) 안에서 yield return null을 쓰면 → 이 루프는 매 프레임 한 번씩 실행

고블린 어택 콜라이더 추가

 

 

고블린 HP 바 - Canvas 고블린에 넣고, World Space 로 설정 후 추가

 

 

 

*ItemManager

using System.Collections.Generic;
using UnityEngine;


public class ItemManager : MonoBehaviour
{
    // 드랍 가능한 아이템 프리팹들을 배열로 등록
    [SerializeField] private GameObject[] items;

    // 지정된 위치에 아이템을 랜덤하게 생성하고 튕겨내는 함수
    public void DropItem(Vector3 dropPos)
    {
        // items 배열 중에서 무작위로 하나를 선택 (0 이상 items.Length 미만)
        var randomIndex = Random.Range(0, items.Length);

        // 해당 위치에 랜덤 아이템 생성 (회전 없음)
        GameObject item = Instantiate(items[randomIndex], dropPos, Quaternion.identity);

        Rigidbody2D itemRb = item.GetComponent<Rigidbody2D>();

        // 좌우 방향으로 약간의 랜덤한 힘을 가함 (좌우로 튕겨지도록)
        itemRb.AddForceX(Random.Range(-2f, 2f), ForceMode2D.Impulse);

        // 위쪽으로 일정한 힘을 가함 (위로 튀어오르게 함)
        itemRb.AddForceY(3f, ForceMode2D.Impulse);

        // 회전력도 랜덤하게 추가하여 아이템이 공중에서 회전하게 만듦
        float ranPower = Random.Range(-1.5f, 1.5f);
        itemRb.AddTorque(ranPower, ForceMode2D.Impulse);
    }
}

 

 

 

 

*knightController_keyboard  (수정된것만)

public class KnightController_Keyboard : MonoBehaviour, IDamageable

private Collider2D knightColl; // 죽었을때 콜라이더 비활성화하기위함
[SerializeField] private Image hpBar;

public float hp = 100f;
public float currHp;

void Start()
{
    animator = GetComponent<Animator>();
    knightRb = GetComponent<Rigidbody2D>();
    knightColl = GetComponent<Collider2D>();  //  콜라이더 컴포넌트 캐싱

    currHp = hp;  // 현재 체력을 최대 체력으로 초기화
    hpBar.fillAmount = currHp / hp;  // 체력바 초기 표시 
}

void OnTriggerEnter2D(Collider2D other)
{
    if (other.CompareTag("Monster"))
    {
        if (other.GetComponent<IDamageable>() != null)  // IDamageable 인터페이스가 구현된 객체인지 확인
        {
            other.GetComponent<IDamageable>().TakeDamage(atkDamage);  // 데미지 적용
            other.GetComponent<Animator>().SetTrigger("Hit");  // 히트 애니메이션 실행
        }
    }
    
}

void InputKeyboard()
{
    float h = Input.GetAxisRaw("Horizontal");
    float v = Input.GetAxisRaw("Vertical");
    inputDir = new Vector3(h, v, 0);

    animator.SetFloat("JoystickX", inputDir.x);
    animator.SetFloat("JoystickY", inputDir.y);

    var capsule = GetComponent<CapsuleCollider2D>();  // 콜라이더 캐싱
    if (inputDir.y < 0)  // 아래 방향키 누르면
    {
        capsule.size = new Vector2(0.7f, 0.3f);  // 콜라이더 크기 축소 (숙이기)
        capsule.offset = new Vector2(0, 0.35f);  // 콜라이더 위치 조정
    }
    else  // 그 외에는 원래 크기
    {
        capsule.size = new Vector2(0.7f, 1.7f);
        capsule.offset = new Vector2(0, 0.85f);
    }
}

public void TakeDamage(float damage)
{
    currHp -= damage;  // 현재 체력 감소
    hpBar.fillAmount = currHp / hp;  // 체력바 비율 갱신

    if (currHp <= 0f)  // 체력이 0 이하이면
        Death();  // 사망 처리 호출
}

public void Death()
{
    animator.SetTrigger("Death");  // 사망 애니메이션 실행
    knightColl.enabled = false;  // 콜라이더 비활성화 (충돌 무시)
    knightRb.gravityScale = 0f;  // 중력 제거 (떨어지지 않도록 고정)
}

 

 

플레이어 hp바 - Canvas UI

  • 내부 Bar는 Source Image 추가해서 Image Type Filled > Horizontal > Left 해주어야함

 

 

 

 

  • 결과물
    : 잘 싸워진다. 땅 콜라이더 설정이 이상해서 끼거나 그러는데 오늘 목표로 한 기능은 모두 구현이 됐다.

추가로 아이템 줍기, 죽으면 hp바 없어지기 등의 기능을 추가할 수 있을것 같다. 죽었을때 좌우로 움직여지는데 destroy하거나 이동을 막거나 Retry 나 Gameover를 넣는 등의 기능을 추가할 수 있을 것 같다. Attack Collider의 범위를 세세하게 조정해볼 수 있을 것 같다.

 

 

 

반응형
salmu
@salmu :: SMU 각종 기록

목차