[패스트캠퍼스 수강 후기] 올인원 패키지 : 유니티 포트폴리오 완성 100% 환급 챌린지 15회차 미션 시작합니다.


원거리 공격 구현입니다.

projectile이라는 "발사체"를 구현해야 합니다. 게임개발에서는 굉장히 많이 사용하는 용어입니다. GameObject를 원하는 방향으로 지속적으로 움직이게하는 컴포넌트라고 보면 됩니다.

 

 



위와 같이 Projectile 컴포넌트 스크립트를 생성하여 코드 작업에 진입합니다. 코드 작업이 바로바로.. ㅎㅎ

public class Projectile : MonoBehaviour
{
    // 변수들
    public float speed;
    public GameObject muzzlePrefab; // muzzle flash라고도 하며, 총 발사시 총구에서 불꽃이 튀는 등의 이팩트를 처리.
    public GameObject hitPrefab; // hit 이펙트. 총알이 벽에 맞았을 때 튀는 형태의 이팩트 처리.
    public AudioClip shotSFX;
    public AudioClip hitSFX;
    private bool collided; // 적에게 부딪혔는지를 확인
    private Rigidbody rigidbody;
    [HideInInspector]
    public AttackBehaviour attackBehaviour; // 공격 파워 등의 정보를 재사용.
    [HideInInspector]
    public GameObject owner; // 발사체의 소유자. 누가 쏜겨!?
    [HideInInspector]
    public GameObject target; // 발사체를 맞은 Gameobject. 누가 맞은겨?
    
    void Start()
    {
        // 타겟 바라보는 방향으로 설정
        if (target != null) {
            Vector3 dest = target.transform.position;
            dest.y += 1.5f;
            transform.LookAt(dest);
        }
        
        if (owner) {
            Collider projectileCollider = GetComponent<Collider>();
            Collider[] ownerColliders = owner.GetComponentsInChildren<Collider>();
            
            foreach (Collider collider in ownerColliders) {
                Physics.IgnoreCollision(projectileCollider, collider);
            }
        }
        
        rigidbody = GetComponent<Rigidbody>();
        
        if (muzzlePrefab != null) {
            GameObject muzzleVFX = Instantiate(muzzlePrefab, transform.position, Quaternion.identity);
            muzzleVFX.transform.forward = gameObject.transform.forward; // 발사 방향 지정
            PaticleSystem particleSystem = muzzleVFX.GetComponent<ParticleSystem>();
            if (particleSystem) {
                Destroy(muzzleVFX, particleSystem.main.duration); // 이팩트 삭제
            }
            else {
                // 자식노드에 달려있는 이팩트도 처리
                ParticleSystem childParticleSystem = muzzleVFX.transform.GetChild(0).GetComponent<ParticleSystem>();
                if (childParticleSystem) {
                    Destroy(muzzleVFX, childSystem.main.duration); // 이팩트 삭제
                }
            }
        }
        
        if (shotSFX != null && GetComponent<AudioSource>()) {
            GetComponent<AudioSource>().PlayOneShot(shotSFX);
        }
    }
    
    private void FixedUpdate()
    {
        if (speed != 0 && rigidbody != null) {
            rigidbody.position += (transform.forward) * (speed * Time.deltaTime);
        }
    }
   
    private void OnCollisionEnter(Collision collision)
    {
        // 여러번 부딪히는 경우를 방지
        if (collided) return;
        collided = true;
        
        Collider projectileCollider = GetComponent<Collider>();
        projectileCollider.enabled = false;
        
        if (hitSFX != null && GetComponent<AudioSource>()) {
            GetComponent<AudioSource>().PlayOneShot(hitSFX);
        }
        
        speed = 0;
        rigidbody.isKinematic = true; // 발사체가 더이상 rigidbody에 의해 위치가 결정되지 않기 때문에 물리엔진 사용하지 않겠다는 의미이고, 더이상 OnCollisionEnter()가 호출되지 않게됨.
        
        ContactPoint contact = collision.contacts[0]; // 첫번째 충돌 지점 얻기.
        Quaternion contactRotation = Quaternion.FromToRotation(Vector3.up, contact.normal);
        Vector3 contactPosition = contact.point;
        
        if (hitPrefab) {
            GameObject hitVFX = Instantiate(hitPrefab, contactPosition, contactRotation);
            PaticleSystem particleSystem = hitVFX.GetComponent<ParticleSystem>();
            if (particleSystem) {
                Destroy(hitVFX, particleSystem.main.duration);
            }
            else {
                // 자식노드에 달려있는 이팩트도 처리
                ParticleSystem childParticleSystem = hitVFX.transform.GetChild(0).GetComponent<ParticleSystem>();
                if (childParticleSystem) {
                    Destroy(hitVFX, childSystem.main.duration);
                }
            }
            
            IDamagable_Original damageable = collision.gameObject.GetComponent<IDamagable_Original>();
            if (damageable != null) {
                damageable.TakeDamage(attackBehaviour?.damage ?? 0, null);
            }
            
            StartCoroutine(DestroyParticle(3.0f));
        }
    }
   
    public IEnuerator DestroyParticle(float waitTime) {
        if (transform.childCount > 0 && waitTime != 0) {
            List<Transform> childs = new List<Transform>();
            foreach (Transform t in transform.GetChild(0).transform) {
                childs.Add(t);
            }
            while (transform.GetChild(0).localScale.x > 0) {
                yield return new waitForSeconds(0.01f);
                transform.GetChild(0).localScale -= new Vector3(0.1f, 0.1f, 0.1f); // 점점 사라지는 효과
                // 자식노드도 동일하게
                for (int i = 0; i < childs.Count; ++i) {
                    childs[i].localScale -= new Vector3(0.1f, 0.1f, 0.1f);
                }
            }
        }
        
        yield return new WaitForSeconds(waitTime);
        Destroy(gameObject);
    }
}





위의 내용중 IgnoreCollision() 함수를 사용한 이유입니다. projectile이 발사하는 GameObject의 뒤쪽에서 시작되는 경우 발사체가 소유자에게 맞아서 이벤트가 끝나버리는 상황이 발생할 수 있기 때문에 모든 경우에 대해 충돌을 무시하도록 처리한 것입니다.





발사체를 구현하기 위해 Sphere GameObject를 만들어 처리합니다. 해당 Sphere에 Projectile 컴포넌트를 드래그하여 가져다 놓으면 위와 같은 설정 내용을 확인 할 수 있습니다.





OnCollisionEnter() 부분을 디버깅 확인하는 방법입니다. Speed값을 올려 Sphere가 벽에 부딪히면 지정된 시간 후에 사라지는 연출이 잘 되고 있는 것을 확인할 수 있습니다.

 



AttackBehaviour_Projectile 컴포넌트 스크립트를 하나 만듭니다.

public class AttackBehaviour_Projectile : AttackBehaviour
{
    public override void ExecuteAttack(GameObject target = null, Transform startPoint = null)
    {
        if (target == null) return;
        
        Vector3 projectilePosition = startPoint?.position ?? transform.position;
        if (effectPrefab) {
            GameObject projectileGO = GameObject.Instantiate<GameObject>(effectPrefab, projectilePosition, Quaternion.identity);
            projectileGO.transform.forward = transform.forward;
            
            Projectile projectile = projectileGO.GetComponent<Projectile>();
            if (projectile) {
                projectile.owner = this.gameObject;
                projectile.target = target;
                projectile.attackBehaviour = this;
            }
            
            calcCoolTime = 0.0f;
        }
    }
}





플레이를 실행해보면 적이 발사체를 발사하는 것을 확인할 수 있고, 열심히 피하면 됩니다만.. ㅋㅋ
한 번 맞아보면 녹색으로 파티클 이팩트가 발생하는 것을 확인할 수 있습니다. 적의 근처로 이동하면 근접공격을 하는 것도 확인할 수 있습니다.



플레이어를 따라다니는 발사체로 변경해보기입니다. 일정시간을 따라오다가 사라지는 발사체입니다.

public class FollowProjectile : Projectile
{
    public float destroyDelay = 5.0f;
    
    protected override void Start()
    {
        base.Start();
        
        StartCoroutine(DestroyParticle(destroyDelay));
    }
    
    protected override void FixedUpdate()
    {
        if (target) {
            Vector3 dest = target.transform.position;
            dest.y += 1.5f;
            transform.LookAt(dest); // 현재 캐릭터의 위치로 다시 설정
        }
        
        base.FixedUpdate();
    }
}




이제 따라다니는 Projectile을 적용해야겠지요?

"Projectile Attack Behaviour"에 있는 "Effect Prefab" 값을 "Projectile" -> "Projectile_Follow"로 변경합니다. 이렇게하여 플레이를 진행하면 아래와 같이 따라오는 발사체가 처리된 것을 확인할 수 있습니다.





여기까지가 한 캐릭터가 여러 공격 행동을 가지는 기능을 구현해 본 것입니다. 이러한 Behaviour 구현 방식은 게임의 여러 요소에서 적용할 수 있는 범위가 다양합니다. 응용범위가 엄청나다고 보면 됩니다. ^^~




<위의 코드들은 제가 보면서 주요한 함수, 코드를 확인하기 위해 타이핑한 용도로, 전체 소스코드가 아님에 주의해 주세요. 전체 코드는 교육 수강을 하면 완벽하게 받으실 수가 있답니다 ^^>

패스트캠퍼스 - 올인원 패키지 : 유니티 포트폴리오 완성 bit.ly/2R561g0

 

유니티 게임 포트폴리오 완성 올인원 패키지 Online. | 패스트캠퍼스

게임 콘텐츠 프로그래머로 취업하고 싶다면, 포트폴리오 완성은 필수! '디아블로'와 '배틀그라운드' 게임을 따라 만들어 보며, 프로그래머 면접에 나오는 핵심 개념까지 모두 잡아 보세요!

www.fastcampus.co.kr

 

  1. 유료강의 코드 올리면 2021.03.22 22:42

    코드올리면 문제생겨요 ;;

    • 잡학지식 BOOX 2021.03.31 02:46 신고

      답변 감사드립니다 ^^~
      현재 올려진 유료 강의 내용은 패스트캠퍼스에서 검수 확인된 내용들입니다.
      추후 패스트캠퍼스에서 언제든 비공개를 요청하면 내려갈 수도 있습니다.

+ Recent posts