2025유니티수업

0707 유니티(좀비 서바이벌)

버스창문 2025. 7. 7. 17:54

레식 방식 재장전

    IEnumerator ReloadRoutine()
    {
        this.State = eState.Reload; // 상태를 Reload로 변경
        this.source.PlayOneShot(gunData.reloadClip); // 장전 사운드 재생
        yield return this.reloadTime; // 장전 시간만큼 대기
        int ammoToReload = Mathf.Min(gunData.magCapacity - this.magAmmo, ammo); // 최대 탄창 용량이 되기위해 필요한 총알 수 또는 남은 총알 수 중 작은 값
        magAmmo += ammoToReload; // 탄창 내 총알 수 증가
        ammo -= ammoToReload; // 남은 총알 수 감소
        this.State = eState.Ready; // 상태를 Ready로 변경
    }

이렇게하면 최대 탄창이 되기위해 남은 탄약만큼만 소모시키면서 재장전을 할 수 있고,

만약 남은 총알이 더 적다면 그것만 재장전 할 수도 있다

 

재장전 로직

public bool Reload()
{
    if (this.State == eState.Reload || this.ammo <= 0 || this.magAmmo >= this.gunData.magCapacity)
        return false;// 이미 장전 중이거나, 남은 총알이 없거나, 탄창이 가득 찼을 때는 장전 불가

    StartCoroutine(ReloadRoutine()); // 장전 코루틴 시작
    return true; // 장전 시작 성공
}

bool로 재장전 성공 여부를 반환시킴


else if (this.input.Reload)
{
    if (this.gun.Reload())// 입력이 있으면 재장전
    {
        //this.animator.SetTrigger("Reload"); // 재장전 애니메이션 트리거 설정
    }
    else
    {
        //재장전을 못할 때 소리가 나거나, 그에 따른 알림 처리
    }
}

bool값을 이용해서 재장전 실패했을때의 처리를 할 수 있다

    private void Shot()
    {
        this.magAmmo--; // 탄창 내 총알 수 감소
        if (this.magAmmo <= 0)
        {
            this.Reload(); // 탄창 내 총알이 0이 되면 장전 시작
            return;
        }
        
        
Shot쪽에서 자동 재장전 되는건 그냥 썼는데 여기서도 사용할 수 있다

 

 

OnAnimatorIK

    private void OnAnimatorIK(int layerIndex)
    {
        //animator IK 를 사용하여 손 위치 조정
        //애니메이터의 실시간 IK업데이트
        this.gunPivot.position = this.animator.GetIKHintPosition(AvatarIKHint.RightElbow); // 오른손 위치에 총 회전 중심점 위치 설정
        this.animator.SetIKPositionWeight(AvatarIKGoal.RightHand, 1f); 
        // 오른손 IK 위치 가중치 설정
        this.animator.SetIKRotationWeight(AvatarIKGoal.RightHand, 1f);
        // 오른손 IK 회전 가중치 설정
        this.animator.SetIKPositionWeight(AvatarIKGoal.LeftHand, 1f);
        // 왼손 IK 위치 가중치 설정
        this.animator.SetIKRotationWeight(AvatarIKGoal.LeftHand, 1f);
        // 왼손 IK 회전 가중치 설정

//가중치 설정 안하고 하면 손 위치가 이상해진다

        this.animator.SetIKPosition(AvatarIKGoal.RightHand, this.rightHandleMount.position);
        this.animator.SetIKPosition(AvatarIKGoal.LeftHand, this.leftHandleMount.position);

        this.animator.SetIKRotation(AvatarIKGoal.RightHand, this.rightHandleMount.rotation);
        this.animator.SetIKRotation(AvatarIKGoal.LeftHand, this.leftHandleMount.rotation);


    }

 

 

 

 

Living Entity (생명체 기반 클래스)

특징

1. 체력이 있다

2. 체력 증감 가능(health pack 을 얻거나, 공격을 맞거나)

3. 살거나 죽거나 할 수 있다.

 

 

체력 UI

 

 

 

 

 

 

 

둥글게 깎이는 체력바

 

 

Living Entity 구현

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

public class LivingEntity : MonoBehaviour,IDamageable
{
    public int maxHp = 100; // 최대 체력
    public int hp { get; protected set; } // 현재 체력
    public bool isDead { get; protected set; } // 생존 여부
    public Action DieAction; // 죽음 이벤트 액션


    protected virtual void OnEnable()
    {
        this.isDead = false;
        this.hp = this.maxHp;
    }
    public virtual void OnDamage(int damage, Vector3 hitPoint, Vector3 hitNormal)
    {
        //체력 있어야함
        //체력이 0이 됐을때 Action 실행

        this.hp -= damage; // 데미지 적용
        if(this.hp <= 0&&!this.isDead)//아직 죽지 않은 상태에서 체력이 0 이하가 되었을때
        {
            Die();
        }
    }

    public virtual void Die()
    {
        this.isDead = true; // 죽음 상태로 변경
        this.DieAction();

        this.gameObject.SetActive(false); // 오브젝트 비활성화
    }
    public virtual void RestoreHealth(int amount)
    {
        if (this.isDead)
        {
            return; // 이미 죽은 상태라면 체력 회복 불가
        }
        this.hp += amount; // 체력 회복
        this.hp = Mathf.Min(this.hp, this.maxHp); // 최대 체력 초과 방지
    }

}

virtual로 만들고, protected를 활용해서 상속된 다음 구현한다.

 

상속 예시


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

public class WomanHealth : LivingEntity
{
    public Slider healthSlider; // 체력 슬라이더 UI
    public AudioClip deathClip; // 죽는 소리
    public AudioClip damageClip; // 데미지 받는 소리
    public AudioClip itemGetClip; // 아이템 획득 소리

    public AudioSource audioSource; // 오디오 소스
    private WomanMovement movement; // 여성 캐릭터의 이동 스크립트
    private WomanShooter shooter; // 여성 캐릭터의 슈팅 스크립트
    private void Awake()
    {
        this.audioSource = GetComponent<AudioSource>();
        this.movement = GetComponent<WomanMovement>();
        this.shooter = GetComponent<WomanShooter>();
    }
    protected override void OnEnable()
    {
        base.OnEnable();
        // 추가적인 초기화 작업이 필요하다면 여기에 작성

    }
    public override void OnDamage(int damage, Vector3 hitPoint, Vector3 hitNormal)
    {
        base.OnDamage(damage, hitPoint, hitNormal);
        // 추가적인 데미지 처리 로직이 필요하다면 여기에 작성

    }
    public override void RestoreHealth(int amount)
    {
        base.RestoreHealth(amount);
        // 추가적인 체력 회복 로직이 필요하다면 여기에 작성
    }
    public override void Die()
    {
        base.Die();
        // 추가적인 죽음 처리 로직이 필요하다면 여기에 작성
        // 예: 사운드 재생, 애니메이션 트리거 등
    }
    private void OnTriggerEnter(Collider other)
    {
        
    }
}

 

 

public class Zombie :LivingEntity
{
    private NavMeshAgent agent; // 네비게이션 에이전트
    private AudioSource source;
    private Animator animator;
    private MeshRenderer meshRenderer; // 메쉬 랜더러

    //오디오소스, 네브메쉬, 콜라이더, 오디오클립, blood Effect, 랜더러

    public LayerMask targetLayer; // 타겟 엔티티 레이어 마스크
    private LivingEntity targetEntity; // 타겟 엔티티

    public ParticleSystem hitEffect; // 피격 이팩트
    public AudioClip hitClip; // 피격 소리
    public AudioClip deathClip; // 죽음 소리

    public int damage = 20;
    public float timeBetweenAttack = 0.5f; // 공격 간격
    private float lastAttackTime; // 마지막 공격 시간

    private readonly int hashHasTarget = Animator.StringToHash("HasTarget"); // 애니메이터 해시
    private readonly int hashDie = Animator.StringToHash("Die"); // 애니메이터 해시


    //추적 대상이 있는지 알려주는 프로퍼티
    private bool hasTarget
    {
        get
        {
            return this.targetEntity != null && !this.targetEntity.isDead;
        }
    }

    void Awake()
    {
        this.source = GetComponent<AudioSource>();
        this.agent = GetComponent<NavMeshAgent>();
        this.animator = GetComponent<Animator>();
        this.meshRenderer = GetComponent<MeshRenderer>();

    }

    public void Setup(ZombieData data) //좀비의 스펙을 결정하는 함수(이동속도, 체력 등)
    {
        this.maxHp = data.hp; // 좀비의 최대 체력 설정
        this.hp = this.maxHp;
        this.damage = data.damage; // 좀비의 공격력 설정
        this.agent.speed = data.moveSpeed; // 네비게이션 에이전트의 이동 속도 설정
        this.meshRenderer.material.color = data.skinColor; // 좀비의 색상 설정
    }
    private void Start()
    {
        StartCoroutine(this.UpdatePath()); // 경로 업데이트 코루틴 시작
    }
    void Update()
    {
        this.animator.SetBool(this.hashHasTarget,this.hasTarget);
    }
    private WaitForSeconds pathUpdateTime = new WaitForSeconds(0.25f);
    IEnumerator UpdatePath()
    {
        while (!this.isDead)
        {
            if (hasTarget)
            {
                this.agent.isStopped = false;
                this.agent.SetDestination(this.targetEntity.transform.position); // 타겟 엔티티의 위치로 경로 설정
            }
            else
            {
                this.agent.isStopped = true; // 타겟이 없으면 이동 중지
                Collider[] cols = Physics.OverlapSphere(transform.position, 10f, this.targetLayer); // 주변의 타겟 엔티티 검색
                for (int i = 0; i < cols.Length; i++)
                {
                    LivingEntity entity = cols[i].GetComponent<LivingEntity>();
                    if (entity != null && !entity.isDead)
                    {
                        this.targetEntity = entity; // 타겟 엔티티 설정
                        break; // 첫 번째 타겟을 찾으면 루프 종료
                    }
                }
            }
            yield return this.pathUpdateTime; // 0.25초마다 경로 업데이트
        }
    }
    public override void OnDamage(int damage, Vector3 hitPoint, Vector3 hitNormal)
    {
        if (!isDead)
        {
            this.hitEffect.transform.position = hitPoint; // 피격 이펙트 위치 설정
            this.hitEffect.transform.rotation = Quaternion.LookRotation(hitNormal); // 피격 이펙트 회전 설정
            this.hitEffect.Play(); // 피격 이펙트 재생
            this.source.PlayOneShot(this.hitClip); // 피격 소리 재생
        }
        base.OnDamage(damage, hitPoint, hitNormal);
    }
    public override void Die()
    {
        base.Die();
        //자신의 모든 콜라이더 비활성화
        Collider[] cols = GetComponents<Collider>(); // 콜라이더 가져오기
        foreach (Collider col in cols)
        {
            col.enabled = false; // 콜라이더 비활성화
        }
        this.agent.isStopped = true; // 네비게이션 에이전트 정지
        this.agent.enabled = false; // 네비게이션 에이전트 비활성화
        this.source.PlayOneShot(this.deathClip); // 죽음 소리 재생
        this.animator.SetTrigger(hashDie); // 죽음 애니메이션 트리거
    }
    public void OnTriggerStay(Collider other)
    {
        //트리거가 충돌한게 추적 대상이라면 공격 실행
        if (!this.isDead && Time.time >= this.lastAttackTime + timeBetweenAttack)
        {
            LivingEntity entity = other.GetComponent<LivingEntity>();
            if(entity != null && this.targetEntity == entity)
            {
                this.lastAttackTime = Time.time; // 마지막 공격 시간 갱신
                Vector3 hitPos = other.ClosestPoint(this.transform.position);
                //상대방 피격 위치와 피격 방향을 근갓값으로 계산
                Vector3 hitNormal = (this.transform.position - other.transform.position);
                // 공격되는 방향을 계산

                entity.OnDamage(this.damage, hitPos, hitNormal.normalized); // 공격 실행

            }
        }
        
    }
}