상속이란?

 

다른 클래스로부터 코드를 물려받는 것

 

상속의 대상 : 클래스의 멤버(필드, 메소드, 프로퍼티 등)

새로 선언하는 클래스 이름 뒤에 클론( : ) 과 기반 클래스의 이름을 표기하여 상속

물려주는 클래스 : 기반/부모 클래스, 물려받는 클래스 : 파생/자식 클래스

 

하나의 클래스가 하나의 상속만 되지만, 전이적으로 여러개를 이어서 사용 가능

 

 

상속을 받을 때 클래스명 뒤에 MonoBehaviour를 지우고 상속받을 부모 클래스의 이름을 추가

 

3개의 오브젝트에 각각의 스크립트를 부착하였다.

 

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

public class Monster : MonoBehaviour
{
    public void Attack()
    {
        Debug.Log("공격");
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Orc : Monster
{
    public void WarCry()
    {
        Debug.Log("전투함성");
        Attack();
    }
    //public void Start()
    //{
    //    WarCry();
    //}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Dragon : Orc
{
    public void Fly()
    {
        Debug.Log("날기");
        Attack();
        WarCry();
    }
    private void Start()
    {
        Fly();
    }
}

실행을 해보면 이런식의 순서로 호출된다.

  1. Dragon 클래스의 Start 메서드가 호출
  2. Start 메서드 내에서 Fly 메서드가 호출
  3. Fly 메서드 내에서 Attack 메서드가 호출. 이때, Dragon 클래스가 Orc 클래스를 상속하고 있으므로,                          Orc 클래스의 Attack 메서드가 실행됩니다.
  4. Attack 메서드가 실행되고 "공격"이라는 메시지가 출력
  5. Fly 메서드 내에서 WarCry 메서드가 호출. WarCry 메서드는 Orc 클래스에 정의되어 있다.
  6. WarCry 메서드가 실행되고 "전투함성"이라는 메시지가 출력된다.
  7. WarCry 메서드 내에서 Attack 메서드가 호출되는데, Orc 클래스가 Monster 클래스를 상속하고 있으므로,        Monster 클래스의 Attack 메서드가 실행됩니다.
  8. Attack 메서드가 실행되고 다시 "공격"이라는 메시지가 출력된다.

쉽게 말해 부모 자식 관계가 Monster -> Oak -> Dragon 순으로 되어있으므로

서로 상속되어 클래스를 가져와 사용할 수 있다.

 

주의

Unity에서 MonoBehaviour를 상속한 클래스에서 Start 메서드를 사용할 때,

상속 관계에 있는 클래스의 Start 메서드 실행 순서는 보장되지 않는다.

이는 Unity 엔진이 각각의 MonoBehaviour를 포함한 오브젝트들에 대해 독립적으로 처리하기 때문이다.

 

 

자식클래스는 부모클래스의 속성을 받아서 사용할 수 있지만

반대로 일반적으로 부모클래스는 자식클래스의 속성과 메소드를 사용할 수 없다.

 


 

다형성이란?

 

 

오류1. WarCry()를 하면 몬스터들의 대미지를 올려주려다 오류가 발생했다.

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

public class Orc : Monster
{
    Monster[] monsters = GameObject.FindObjectsOfType<Monster>();
    public void WarCry()
    {
        Debug.Log("전투함성");

        //모든 몬스터 공격력 + 10 증가
       
        for(int i = 0; i < monsters.Length; i++)
        {
            monsters[i].damage += 10;
            Debug.LogFormat("추가된 공격력은 {0}", monsters[i].damage);
        }

    }
    private void Start()
    {
        for (int i = 0; i < monsters.Length; i++)
        {
            Debug.LogFormat("기본 공격력은 {0}", monsters[i].damage);
        }
        WarCry();
    }

}

 

 

이것에 대해 알아보니

Unity에서는 MonoBehaviour의 생성자나 인스턴스 필드 초기화자에서 FindObjectsOfType를 호출하는 것을

허용하지 않으므로 대신에 Awake나 Start 메서드에서 호출해야 한다.

따라서 Orc 클래스의 생성자에서 FindObjectsOfType를 호출하는 것은 허용되지 않는다.

Orc 클래스의 WarCry 메서드 내에서 호출하거나, Orc 클래스의 Awake나 Start 메서드에서 호출해야 한다고 한다.

 

오류2. 호출이 2번된다.

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

public class Monster : MonoBehaviour
{
    public float damage = 10f;
    public void Attack()
    {
        Debug.Log("공격");
    }

}

 

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

public class Orc : Monster
{
    public void WarCry()
    {
        Debug.Log("전투함성");

        //모든 몬스터 공격력 + 10 증가
        Monster[] monsters = GameObject.FindObjectsOfType<Monster>();

        for (int i = 0; i < monsters.Length; i++)
        {
            monsters[i].damage += 10;
            Debug.LogFormat("추가된 공격력은 {0}", monsters[i].damage);
        }

    }
    private void Start()
    {
        Monster[] monsters = GameObject.FindObjectsOfType<Monster>();

        for (int i = 0; i < monsters.Length; i++)
        {
            Debug.LogFormat("기본 공격력은 {0}", monsters[i].damage);
        }
        WarCry();
    }

}

 

주어진 코드에서 Orc 클래스의 Start 메서드에서 WarCry 메서드를 호출하기 전에

FindObjectsOfType<Monster>()를 사용하여 몬스터 배열을 얻고 있다.

FindObjectsOfType<Monster>()는 현재 씬에 있는 모든 Monster 클래스의 인스턴스를 찾아 배열로 반환한다.

Start 메서드에서 이를 호출하여 몬스터 배열을 가져오고,

그 다음에 WarCry 메서드를 호출하는 것은 상황에 따라 문제가 될 수 있다.

만약 Start 메서드가 호출될 때 씬에 이미 생성된 몬스터가 있다면,

WarCry 메서드에서 다시 같은 몬스터 배열을 가져오게 되어 몬스터 배열이 중복되어 보일 수 있다.

따라서 Orc 클래스의 Start 메서드에서 WarCry 메서드를 호출하기 전에 몬스터 배열을 가져오는 부분을 제거하면

이런 문제를 해결할 수 있고, WarCry 메서드에서만 몬스터 배열을 가져오도록하면 된다.

 

 

여기서 자세히 보아야할 것은 다형성을 이용한 패턴이라는 것이다.

 

 

https://docs.unity3d.com/ScriptReference/Object.FindObjectsOfType.html

 

Unity - Scripting API: Object.FindObjectsOfType

This does not return assets (such as meshes, textures or prefabs), or objects with HideFlags.DontSave set. Objects attached to inactive GameObjects are only included if inactiveObjects is set to true. Use Resources.FindObjectsOfTypeAll to avoid these limit

docs.unity3d.com

 

명시한 타입의 모든 오브젝트를 찾아 배열로 반환, 씬에 있는 모든 Monster 타입의 오브젝트가 monsters배열에 저장됨

'낙서장 > C#' 카테고리의 다른 글

[C#] 메서드 오버로딩  (0) 2024.06.02
virtual ,override, base  (2) 2024.03.08
흐름 제어  (1) 2024.02.26
데이터를 가공하는 연산자  (0) 2024.02.25
데이터를 담는 변수와 상수  (0) 2024.02.22

 

 

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

namespace yjShin
{
    public class Woman : MonoBehaviour
    {
        private void OnAnimatorIK(int layerIndex)
        {
            Debug.Log("OnAnimatorIK");
        }
    }

}

 

 

 

 

 

오른쪽 팔꿈치의 좌표를 확인

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

namespace yjShin
{
    public class Woman : MonoBehaviour
    {
        private Animator anim;

        private void Start()
        {
            anim = GetComponent<Animator>();
        }
        private void OnAnimatorIK(int layerIndex)
        {
            //Debug.Log("OnAnimatorIK");
            Vector3 rightElbowPosition = anim.GetIKHintPosition(AvatarIKHint.RightElbow);
            Debug.Log(rightElbowPosition);
        }
    }

}

애니메이터에 저장되어 있는 피봇들

 

AvatarIKHint.RightElbow는 애니메이션 시스템에서 제공되는 미리 정의된 상수로, 이를 통해 오른쪽 팔꿈치의 위치를 얻을 수 있습니다. 이 값은 애니메이션 시스템 내부에서 이미 정의되어 있으며, 개발자가 직접 오른쪽 팔꿈치를 찾거나 계산할 필요가 없습니다.

이제 건의 pivot 위치를 잡아 줄 것이다.

 

 [SerializeField] private Transform gunPivot;

 

건의 피봇에 오른쪽 팔꿈치의 위치를 넣어줄 것이다.

건의 피봇의 위치가 팔꿈치에 맞춰졌지만 건의 위치는 다르게 위에 있다.

 

 

건의 위치를 바로 잡아주기 위해

총의 드래그해서 적절한 위치에 이동 시킨후 좌표를 바꿔준다.

 

 

그 후 건의 건을 누르고 복사

 

복사된 좌표를 건에다 붙여준다.

 

 

이제 파지를 할 왼손을 넣어줄 것이다.

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

namespace smilejsu
{
    public class Woman : MonoBehaviour
    {
        private Animator anim;
        [SerializeField] private Transform gunPivot;
        [SerializeField] private Transform leftHandMount;   //왼쪽 손잡이 

        private void Start()
        {
            this.anim = GetComponent<Animator>();    
        }

        private void OnAnimatorIK(int layerIndex)
        {
            Vector3 rightElbowPosition = anim.GetIKHintPosition(AvatarIKHint.RightElbow);
            gunPivot.position = rightElbowPosition;

            this.anim.SetIKPosition(AvatarIKGoal.LeftHand, this.leftHandMount.position);
            this.anim.SetIKRotation(AvatarIKGoal.LeftHand, this.leftHandMount.rotation);
        }
    }

}

 

그 후 인스펙터에 할당

 

 

 

이제 왼쪽팔의 무게와 회전을 Range로 넣어줄 것이다.

 

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

namespace yjShin
{
    public class Woman : MonoBehaviour
    {
        private Animator anim;
        [SerializeField] private Transform gunPivot;
        [SerializeField] private Transform leftHandMount; //왼쪽 손잡이

        [Range(0,1)]
        [SerializeField] private float leftHandPositionWeight = 1f;

        [Range(0, 1)]
        [SerializeField] private float leftHandRotationWeight = 1f;

        private void Start()
        {
            anim = GetComponent<Animator>();
        }
        private void OnAnimatorIK(int layerIndex)
        {
            //Debug.Log("OnAnimatorIK");
            Vector3 rightElbowPosition = anim.GetIKHintPosition(AvatarIKHint.RightElbow);
            //Debug.Log(rightElbowPosition);
            gunPivot.position = rightElbowPosition;

            this.anim.SetIKPosition(AvatarIKGoal.LeftHand, this.leftHandMount.position);
            this.anim.SetIKRotation(AvatarIKGoal.LeftHand, this.leftHandMount.rotation);

            this.anim.SetIKPositionWeight(AvatarIKGoal.LeftHand, this.leftHandPositionWeight);
            this.anim.SetIKRotationWeight(AvatarIKGoal.LeftHand, this.leftHandRotationWeight);
        }
    }

}

Animator들의 속성에 대해 모르는 점이 있으면 다큐멘터리를 적극이용하자

https://docs.unity3d.com/ScriptReference/Animator.html

 

Unity - Scripting API: Animator

Success! Thank you for helping us improve the quality of Unity Documentation. Although we cannot accept all submissions, we do read each suggested change from our users and will make updates where applicable. Close

docs.unity3d.com

 

 

그래서 만들어진 완성본

 

 

 

 

 

 

 

using UnityEngine;

// 주어진 Gun 오브젝트를 쏘거나 재장전
// 알맞은 애니메이션을 재생하고 IK를 사용해 캐릭터 양손이 총에 위치하도록 조정
public class PlayerShooter : MonoBehaviour {
    public Gun gun; // 사용할 총
    public Transform gunPivot; // 총 배치의 기준점
    public Transform leftHandMount; // 총의 왼쪽 손잡이, 왼손이 위치할 지점
    public Transform rightHandMount; // 총의 오른쪽 손잡이, 오른손이 위치할 지점

    private PlayerInput playerInput; // 플레이어의 입력
    private Animator playerAnimator; // 애니메이터 컴포넌트

    private void Start() {
        // 사용할 컴포넌트들을 가져오기
        playerInput = GetComponent<PlayerInput>();
        playerAnimator = GetComponent<Animator>();
    }

    private void OnEnable() {
        // 슈터가 활성화될 때 총도 함께 활성화
        gun.gameObject.SetActive(true);
    }
    
    private void OnDisable() {
        // 슈터가 비활성화될 때 총도 함께 비활성화
        gun.gameObject.SetActive(false);
    }

    private void Update() {
        // 입력을 감지하고 총 발사하거나 재장전
        if (playerInput.fire)
        {
            gun.Fire();
        }
        else if (playerInput.reload)
        {
            {
                if (gun.Reload())
                {
                }
                playerAnimator.SetTrigger("Reload");
            }
        }
        UpdateUI();
    }

    // 탄약 UI 갱신
    private void UpdateUI() {
        if (gun != null && UIManager.instance != null)
        {
            // UI 매니저의 탄약 텍스트에 탄창의 탄약과 남은 전체 탄약을 표시
            UIManager.instance.UpdateAmmoText(gun.magAmmo, gun.ammoRemain);
        }
    }

    // 애니메이터의 IK 갱신
    private void OnAnimatorIK(int layerIndex) {

        //총의 기준점 gunPivot을 3D 모델의 오른쪽 팔꿈치 위치로 이동
        gunPivot.position = playerAnimator.GetIKHintPosition(AvatarIKHint.RightElbow);

        //IK를 사용하여 왼손의 위치와 회전을 총의 왼쪽 손잡이에 맞춤
        playerAnimator.SetIKPositionWeight(AvatarIKGoal.LeftHand, 1.0f);
        playerAnimator.SetIKRotationWeight(AvatarIKGoal.LeftHand, 1.0f);

        playerAnimator.SetIKPosition(AvatarIKGoal.LeftHand, leftHandMount.position);
        playerAnimator.SetIKRotation(AvatarIKGoal.LeftHand, leftHandMount.rotation);
        //IK를 사용하여 오른손의 위치와 회전을 총의 오른쪽 손잡이에 맞춤
        playerAnimator.SetIKPositionWeight(AvatarIKGoal.RightHand, 1.0f);
        playerAnimator.SetIKRotationWeight(AvatarIKGoal.RightHand, 1.0f);

        playerAnimator.SetIKPosition(AvatarIKGoal.RightHand, rightHandMount.position);
        playerAnimator.SetIKRotation(AvatarIKGoal.RightHand, rightHandMount.rotation);

    }
}

 

using System.Collections;
using UnityEngine;
using UnityEngine.UIElements;

// 총을 구현
public class Gun : MonoBehaviour
{
    // 총의 상태를 표현하는 데 사용할 타입을 선언
    public enum State
    {
        Ready, // 발사 준비됨
        Empty, // 탄알집이 빔
        Reloading // 재장전 중
    }

    public State state { get; private set; } // 현재 총의 상태

    public Transform fireTransform; // 탄알이 발사될 위치

    public ParticleSystem muzzleFlashEffect; // 총구 화염 효과
    public ParticleSystem shellEjectEffect; // 탄피 배출 효과

    private LineRenderer bulletLineRenderer; // 탄알 궤적을 그리기 위한 렌더러

    private AudioSource gunAudioPlayer; // 총 소리 재생기

    public GunData gunData; // 총의 현재 데이터

    private float fireDistance = 50f; // 사정거리

    public int ammoRemain = 100; // 남은 전체 탄알
    public int magAmmo; // 현재 탄알집에 남아 있는 탄알

    private float lastFireTime; // 총을 마지막으로 발사한 시점

    private void Awake() {
        // 사용할 컴포넌트의 참조 가져오기
        gunAudioPlayer = GetComponent<AudioSource>();
        this.bulletLineRenderer = GetComponent<LineRenderer>();
        this.bulletLineRenderer.positionCount = 2;
        this.bulletLineRenderer.enabled = false;
    }

    private void OnEnable() {
        ammoRemain = gunData.startAmmoRemain;
        magAmmo = gunData.magCapacity;
        // 총 상태 초기화
        this.state = State.Ready;
        lastFireTime = 0;
    }

    // 발사 시도
    public void Fire() {
        if(this.state == State.Ready && Time.time >= lastFireTime + gunData.timeBetFire)
        {
            lastFireTime = Time.time;
            Shot();
        }
    }

    // 실제 발사 처리
    private void Shot() {
        RaycastHit hit;
        Vector3 hitPosition =Vector3.zero;
        //시작점, 방향, 히트정보, 거리
        if(Physics.Raycast(fireTransform.position, fireTransform.forward, out hit, fireDistance))
        {
            //충돌한 콜라이더의 게임오브젝트에 붙어 있는 IDamageable 인터페이스를 상속받고 있는 컴포넌트를 가져온다.
            IDamageable target = hit.collider.GetComponent<IDamageable>();
            if (target != null)
            {
                target.OnDamage(gunData.damage, hit.point, hit.normal);
            }
            hitPosition = hit.point;
        }
        else
        {
            //탄알이 최대 사정거리까지 날아갔을 때의 위치를 충돌 위치로 사용 => out hit가 빠진다.
            hitPosition = fireTransform.position + fireTransform.forward * fireDistance;
        }
        StartCoroutine(ShotEffect(hitPosition));
        magAmmo--;
        if(magAmmo <= 0)
        {
            state = State.Empty;
        }
    }

    // 발사 이펙트와 소리를 재생하고 탄알 궤적을 그림
    private IEnumerator ShotEffect(Vector3 hitPosition) {

        muzzleFlashEffect.Play();
        shellEjectEffect.Play();

        //총격 소리 재생
        gunAudioPlayer.PlayOneShot(gunData.shotClip);

        //라인랜더러의 시작 위치
        this.bulletLineRenderer.SetPosition(0, fireTransform.position);

        //라인랜더러의 끝 위치
        this.bulletLineRenderer.SetPosition(1, hitPosition);

        // 라인 렌더러를 활성화하여 탄알 궤적을 그림
        bulletLineRenderer.enabled = true;

        // 0.03초 동안 잠시 처리를 대기
        yield return new WaitForSeconds(0.03f);

        // 라인 렌더러를 비활성화하여 탄알 궤적을 지움
        bulletLineRenderer.enabled = false;
    }

    // 재장전 시도
    public bool Reload()
    {
        if (state == State.Reloading || ammoRemain <= 0 || magAmmo >= gunData.magCapacity)
        {
            return false;
        }
        StartCoroutine(ReloadRoutine());
        return true;
    }

    // 실제 재장전 처리를 진행
    private IEnumerator ReloadRoutine() {
        // 현재 상태를 재장전 중 상태로 전환
        state = State.Reloading;

        gunAudioPlayer.PlayOneShot(gunData.reloadClip);
        // 재장전 소요 시간 만큼 처리 쉬기
        yield return new WaitForSeconds(gunData.reloadTime);

        int ammoToFill = gunData.magCapacity - magAmmo;
        
        if(ammoRemain < ammoToFill)
        {
            ammoToFill = ammoRemain;
        }
        magAmmo += ammoToFill;
        ammoRemain -= ammoToFill;
        // 총의 현재 상태를 발사 준비된 상태로 변경
        state = State.Ready;
    }

    private void Update()
    {
        //test
        if(Input.GetMouseButtonDown(0))
        {
            this.Fire();
        }
    }

}

 

using UnityEngine;

// 데미지를 입을 수 있는 타입들이 공통적으로 가져야 하는 인터페이스
public interface IDamageable {
    // 데미지를 입을 수 있는 타입들은 IDamageable을 상속하고 OnDamage 메서드를 반드시 구현해야 한다
    // OnDamage 메서드는 입력으로 데미지 크기(damage), 맞은 지점(hitPoint), 맞은 표면의 방향(hitNormal)을 받는다
    void OnDamage(float damage, Vector3 hitPoint, Vector3 hitNormal);
}
using UnityEngine;

[CreateAssetMenu(menuName = "Scriptable/GunData", fileName = "Gun Data")]
public class GunData : ScriptableObject
{
    public AudioClip shotClip; // 발사 소리
    public AudioClip reloadClip; // 재장전 소리

    public float damage = 25; // 공격력

    public int startAmmoRemain = 100; // 처음에 주어질 전체 탄약
    public int magCapacity = 25; // 탄창 용량

    public float timeBetFire = 0.12f; // 총알 발사 간격
    public float reloadTime = 1.8f; // 재장전 소요 시간
}

'산대특 > 게임 클라이언트 프로그래밍' 카테고리의 다른 글

Sprite Shooter - Player and Monster Animation  (0) 2024.03.14
Sprite Shooter - Destroy Barrel and Layer  (0) 2024.03.13
Zombie - Line Render  (0) 2024.03.07
Tank - Rigidbody.MovePosition  (0) 2024.03.06
UniRun  (4) 2024.03.05

 

 

 

 

 

 

인터페이스를 사용하는 이유:

 

사용하지 않으면 다른 클래스의 타입을 모두 검사해야하기 때문에

즉, OnDamage()메서드를 실행하기만 하면 되서

 

 

 

얘네들은 원래 Gun안에 있어야 하지만.

 

이렇게 바꿔서 스크립터블 오브젝트로 동작할 수 있도록 상속

에셋을 생성하는 메뉴를 만들기 위해 특성을 클래스에 추가

CreateAssetMenu는 스크립터블 오브젝트 타입에 추가할 수 있다.

해당 타입의 에셋을 생성할 수 있는 버튼을 Assets와 Create(+버튼) 메뉴에 추가

"(경로)"  "(기본 파일명)", order =메뉴 상에서 순서

 

 

이번에 중점적으로 볼 것은 라인렌더이다. => Gun.cs

라인 렌더러 사이즈 2로 바꾼후 수치를 입력하면 좌표로 쏜다는 걸 알 수 있다.

 

 

 

this.bulletLineRenderer.enabled = false;

 

 

 

 

선을 그려서 총을 쏠때 방향을 확인해야 한다.

 

 

hit.normal => 노말을 쓴다는 건 애니메이션 방향을 정해주기 위해?

 

 

else 문은 타겟이 맞지 않았을 때 ex)허공에 쏠 때

탄알의 궤적을 잡아주려하기 위해

 

실행을 하면 Null 오류가 날 것인데 그러면 if로 조건을 넣어주면 된다.

using UnityEngine;

[CreateAssetMenu(menuName = "Scriptable/GunData", fileName = "Gun Data")]
public class GunData : ScriptableObject
{
    public AudioClip shotClip; // 발사 소리
    public AudioClip reloadClip; // 재장전 소리

    public float damage = 25; // 공격력

    public int startAmmoRemain = 100; // 처음에 주어질 전체 탄약
    public int magCapacity = 25; // 탄창 용량

    public float timeBetFire = 0.12f; // 총알 발사 간격
    public float reloadTime = 1.8f; // 재장전 소요 시간
}

 

using System.Collections;
using UnityEngine;
using UnityEngine.UIElements;

// 총을 구현
public class Gun : MonoBehaviour
{
    // 총의 상태를 표현하는 데 사용할 타입을 선언
    public enum State
    {
        Ready, // 발사 준비됨
        Empty, // 탄알집이 빔
        Reloading // 재장전 중
    }

    public State state { get; private set; } // 현재 총의 상태

    public Transform fireTransform; // 탄알이 발사될 위치

    public ParticleSystem muzzleFlashEffect; // 총구 화염 효과
    public ParticleSystem shellEjectEffect; // 탄피 배출 효과

    private LineRenderer bulletLineRenderer; // 탄알 궤적을 그리기 위한 렌더러

    private AudioSource gunAudioPlayer; // 총 소리 재생기

    public GunData gunData; // 총의 현재 데이터

    private float fireDistance = 50f; // 사정거리

    public int ammoRemain = 100; // 남은 전체 탄알
    public int magAmmo; // 현재 탄알집에 남아 있는 탄알

    private float lastFireTime; // 총을 마지막으로 발사한 시점

    private void Awake() {
        // 사용할 컴포넌트의 참조 가져오기
        gunAudioPlayer = GetComponent<AudioSource>();
        this.bulletLineRenderer = GetComponent<LineRenderer>();
        this.bulletLineRenderer.positionCount = 2;
        this.bulletLineRenderer.enabled = false;
    }

    private void OnEnable() {
        ammoRemain = gunData.startAmmoRemain;
        magAmmo = gunData.magCapacity;
        // 총 상태 초기화
        this.state = State.Ready;
        lastFireTime = 0;
    }

    // 발사 시도
    public void Fire() {
        if(this.state == State.Ready && Time.time >= lastFireTime + gunData.timeBetFire)
        {
            lastFireTime = Time.time;
            Shot();
        }
    }

    // 실제 발사 처리
    private void Shot() {
        RaycastHit hit;
        Vector3 hitPosition =Vector3.zero;
        //시작점, 방향, 히트정보, 거리
        if(Physics.Raycast(fireTransform.position, fireTransform.forward, out hit, fireDistance))
        {
            //충돌한 콜라이더의 게임오브젝트에 붙어 있는 IDamageable 인터페이스를 상속받고 있는 컴포넌트를 가져온다.
            IDamageable target = hit.collider.GetComponent<IDamageable>();
            if (target != null)
            {
                target.OnDamage(gunData.damage, hit.point, hit.normal);
            }
            hitPosition = hit.point;
        }
        else
        {
            //탄알이 최대 사정거리까지 날아갔을 때의 위치를 충돌 위치로 사용 => out hit가 빠진다.
            hitPosition = fireTransform.position + fireTransform.forward * fireDistance;
        }
        StartCoroutine(ShotEffect(hitPosition));
        magAmmo--;
        if(magAmmo <= 0)
        {
            state = State.Empty;
        }
    }

    // 발사 이펙트와 소리를 재생하고 탄알 궤적을 그림
    private IEnumerator ShotEffect(Vector3 hitPosition) {

        muzzleFlashEffect.Play();
        shellEjectEffect.Play();

        //총격 소리 재생
        gunAudioPlayer.PlayOneShot(gunData.shotClip);

        //라인랜더러의 시작 위치
        this.bulletLineRenderer.SetPosition(0, fireTransform.position);

        //라인랜더러의 끝 위치
        this.bulletLineRenderer.SetPosition(1, hitPosition);

        // 라인 렌더러를 활성화하여 탄알 궤적을 그림
        bulletLineRenderer.enabled = true;

        // 0.03초 동안 잠시 처리를 대기
        yield return new WaitForSeconds(0.03f);

        // 라인 렌더러를 비활성화하여 탄알 궤적을 지움
        bulletLineRenderer.enabled = false;
    }

    // 재장전 시도
    public bool Reload()
    {
        if (state == State.Reloading || ammoRemain <= 0 || magAmmo >= gunData.magCapacity)
        {
            return false;
        }
        StartCoroutine(ReloadRoutine());
        return true;
    }

    // 실제 재장전 처리를 진행
    private IEnumerator ReloadRoutine() {
        // 현재 상태를 재장전 중 상태로 전환
        state = State.Reloading;

        gunAudioPlayer.PlayOneShot(gunData.reloadClip);
        // 재장전 소요 시간 만큼 처리 쉬기
        yield return new WaitForSeconds(gunData.reloadTime);

        int ammoToFill = gunData.magCapacity - magAmmo;
        
        if(ammoRemain < ammoToFill)
        {
            ammoToFill = ammoRemain;
        }
        magAmmo += ammoToFill;
        ammoRemain -= ammoToFill;
        // 총의 현재 상태를 발사 준비된 상태로 변경
        state = State.Ready;
    }

    private void Update()
    {
        //test
        if(Input.GetMouseButtonDown(0))
        {
            this.Fire();
        }
    }

}

하이어라이키에 있는 프리팹을 Gun프리팹에 모두 할당시키기 위해 Apply All

 

 

 

'산대특 > 게임 클라이언트 프로그래밍' 카테고리의 다른 글

Sprite Shooter - Destroy Barrel and Layer  (0) 2024.03.13
Zombie - IK  (0) 2024.03.07
Tank - Rigidbody.MovePosition  (0) 2024.03.06
UniRun  (4) 2024.03.05
Quaternion  (1) 2024.03.04

+ Recent posts