[패스트캠퍼스 수강 후기] 올인원 패키지 : 유니티 포트폴리오 완성 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

 

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


저번에 이어 전투시스템 구현입니다.
IAttackable에 대한 인터페이스 함수 구현을 완료하였고, 이번에는 IDamageable에 대한 인터페이스 함수를 구현합니다.

구성되어 있던 IDamageable 인터페이스는 다음과 같았습니다.
public interface IDamageable
{
    bool IsAlive { get; }
    void TakeDamage(int damage, GameObject hitEffectPrefabs);
}


이를 EnemyController_New 스크립트에 구현을 해줍니다.

public class EnemyController_New : EnemyController, IAttackable, IDamageable
{
    // 데미지 관련 체력 변수 및, 초기화 등을 해줍니다.
    
    public Transform hitTransform; // Hit Effect가 발생했을 때의 위치값.
    public int maxHealth = 100; // 최대 체력. 레벨 올라감에 따른 최대 체력 증가 고려.
    public int health
    {
        get;
        private set;
    }
    
    protected override void Start()
    {
        // 기존 구현부..
        
        health = maxHealth;
    }
    

    // IDamageable 구현
    bool IsAlive => health > 0; // 체력 0이상으로 살아있음을 체크
    
    void TakeDamage(int damage, GameObject hitEffectPrefabs)
    {
        if (!IsAlive) return;
        
        health -= damage;
        if (hitEffectPrefabs)
        {
            Instantiate(hitEffectPrefabs, hitTransform);
        }
        
        if (IsAlive)
            animator?.SetTrigger(hitTriggerHash); // 피격 애니메이션 실행
        else
            stateMachine.ChangeState<DeadState>();
    }
}



AttackState_New.cs

public class AttackState_New : State<EnemyController>
{
    private Animator animator;
    private AttackStateController attackStateController;
    private IAttackable attackable;
    
    protected int attackTriggerHash = Animator.StringToHash("AttackTrigger");
    protected int attackIndexHash = Animator.StringToHash("AttackIndex");
    
    pubilc override void OnInitialized()
    {
        animator = context.GetComponent<Animator>();
        attackStateController = context.GetComponent<AttackStateController>();
        attackable = context.GetComponent<IAttackable>();
    }
    
    public override void OnEnter()
    {
        // IAttackable이 구현되어 있지 않았거나, 구현되어 있지만 CoolTime이 안되었다면..
        if (attackable == null || attackable.CurrentAttackBehaviour == null) {
            statemachine.ChangeState<IdleState>();
            return;
        }
        
        attackStateController.enterAttackStateHandler += OnEnterAttackState();
        attackStateController.exitAttackStateHandler += OnExitAttackState();

        animator?.SetInteger(attackIndexHash, attackable.CurrentAttackBehaviour.animationIndex);
        animator?.SetTrigger(attackTriggerHash);
    }
    
    public void OnEnterAttackState()
    {
        // 해당 애니메이션만 변경되므로 처리할 내용은 없음.
    }
    
    public void OnExitAttackState()
    {
        stateMachine.ChangeState<IdleState>();
    }
}




StateMachine이 구현된 상태입니다.




원거리 공격을 위한 projectilePoint이 설정한 화면입니다.

해당 적 캐릭터는 근거리, 원거리 공격 둘 다 가능하므로 Projectile Attack Behaviour와 Melee Attack Behaviour를 둘다 구현해 주면 됩니다.

내용이 완전 쉽지는 않고 캡슐화를 위해 여러 컴포넌트 스크립트와 클래스, 인터페이스들로 인해 좀 정신이 없기는 합니다. 사실 이런 부분은 해당 내용 및 코드를 완벽히 이해한 상태에서 직접 구현해봐야 이해할 수 있습니다.



근접 공격에 대한 내용입니다.
근접 공격에 대한 충돌은 ManualCollision 컴포넌트 스크립트로 자체 구현합니다.


ManualCollision_New.cs

public class ManualCollision_New : MonoBehaviour
{
    public Vector3 boxSize = new Vector3(3, 2, 2);
    
    public Collider[] CheckOverlapBox(LayerMask layerMask)
    {
        return Physics.OverlapBox(transform.position, boxSize * 0.5f, transform.rotation, layerMask);
    }
    
    private void OnDrawGizmos()
    {
        Gizmos.matrix = transform.localToWorldMatrix; // Gizmos의 상태 위치를 World 위치로..
        Gizmos.color = Color.yellow;
        Gizmos.DrawWireCube(Vector3.zero, boxSize);
    }
}




MeleeAttackCollision을 Amphisbaena라는 적 캐릭터의 자식 컴포넌트로 등록하고, 그 안에서 "Manual Collision" 컴포넌트를 등록하여 사용합니다. 자식 노트로 한 이유는 캐릭터를 이동할 때 Collision 영역이 캐릭터를 따라다녀야 편하기 때문이지요.
이제 근접 공격 스크립트를 작성합니다.

AttackBehaviour_Melee.cs

public class AttackBehaviour_Melee : AttackBehaviour
{
    public manualCollision attackCollision;
    
    public override void ExecuteAttack(GameObject target = null, Transform startPoint = null)
    {
        Collider[] colliders = attackCollision?.CheckOverlapBox(targetMask);
        
        foreach (Collider collider in colliders)
        {
            collider.gameObject.GetComponent<IDamagable_Original>()?.TakeDamage(damage, effectPrefabs);
        }
    }
}




드디어 유니티 실행입니다. 플레이를 시작하면 적캐릭터가 따라오며 근접공격이 가능한 경우 근접 공격 애니메이션이 발생됨을 볼 수 있습니다.



실제 해당 공격이 발생되는 시점에 Manual Collision을 보면 캐릭터가 해당 영역 (boxSize)내에 있기 때문에 Damage를 받아 체력이 감소하는 처리를 하게 됩니다.

다음 번에는 원거리 공격을 구현할 예정입니다.



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

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

 

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

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

www.fastcampus.co.kr

 

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




어느덧 챕터 5 진입이네요.

전투 시스템에 대하여 알아보는 시간입니다.

배우게 될 전체 구성 순서는 다음과 같습니다.

1. 전투 시스템 구성 알아보기
2. 전투 시스템 구현하기
3. 근접 전투 로직 구현하기
4. 원거리 전투 로직 구현하기
5. NPC의 HP UI 구현하기




EnemyController는 기존에 구현하였던 플레이어 캐릭터나 적캐릭터라고 보면 됩니다. 여기서는 Idle, Attack, Move State만 가지는 Controller를 구현합니다.
AttackStateController는 Animator에서 공격중인지 상태를 알게해주고, 이를 확인하여 EnemyController Logic에 전달하여 상태를 확인하도록 구현합니다. 물론 EnemyController 컴포넌트에서 직접 작업하여도 되지만, AttackStateController를 재활용할 수 있도록 별도의 컴포넌트로 제작을 하는 것입니다.
IAttackable은 Animator에서 공격이 발생하는 시점에 호출이되는 이벤트 인터페이스입니다. IAttackable 인터페이스를 따로 구현한 이유 역시 공격가능한 GameObject에 쉽게 부착하여 구현할 수 있도록 구성한 것입니다. 정말 좋은 내용입니다.
IDamageable도 마찬가지이지요. 데미지를 받아 에너지가 줄어드는 GameObject인 경우 적용합니다.
인터페이스의 장점은 클래스 상속과 상관없이 Attach/Dettach가 가능하고, 식별이 용이하다는 것이 장점입니다. 단점이라면 구조가 복잡해질 수 있다는 점입니다. Interface를 컴포넌트로 구성할 수도 있지만 참조가 많이 발생하게 되어 복잡성이 높아질 수 있어서 여기서는 Interface로만 구현합니다.
AttackBehaviour_xx는 공격 종류라고 보면 됩니다. EnemyController에서 여러 공격의 종류 중 하나를 선택하여 공격이 발생하는 시스템입니다.


AttackStateController와 Animator를 먼저 구성해 봅니다.



StateMachine은 위와 같이 구성합니다.


AttackStateController.cs

public class AttackStateController : MonoBehaviour
{
    public delegate void OnEnterAttackState();
    public delegate void OnExitAttackState();
    
    public OnEnterAttackState enterAttackStateHandler;
    public OnExitAttackState exitAttackStatehandler;
    
    public bool IsInAttackState
    {
        get;
        private set; // 외부에서 값 설정 불가.
    }
    
    void Start()
    {
        enterAttackStateHandler = new OnEnterAttackState(EnterAttackState);
        exitAttackStateHandler = new OnExitAttackState(ExitAttackState);
    }
    
    public void OnStartOfAttackState()
    {
        IsInAttackState = true;
        enterAttackStateHandler();
    }
    
    public void OnEndOfAttackState()
    {
        IsInAttackState = false;
        exitAttackStateHandler();
    }
    
    private void EnterAttackState()
    {
    }
    
    private void ExitAttackState()
    {
    }
    
    // Animator에서 호출할 함수
    public void OnCheckAttackCollider(int attackIndex)
    {
        GetComponent<IAttackable>()?.OnExecuteAttack(attackIndex);
    }
}

 



AttackStateMachineBehaviour.cs

public class AttackStateMachineBehaviour : StateMachineBehaviour
{
    public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
        animator.getObject.GetComponent<AttackStateController>()?.OnStartOfAttackState();
    }
    
    public override void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
        animator.getObject.GetComponent<AttackStateController>()?.OnEndOfAttackState();
    }
}

 



IAttackable.cs

public interface IAttackable
{
    AttackBehaviour CurrentAttackBehaviour
    {
        get;
    }
    
    void OnExecuteAttack(int attackIndex);
}


IDamageable.cs

public interface IDamageable
{
    bool IsAlive
    {
        get;
    }
    
    void TakeDamage(int damage, GameObject hitEffectPrefabs);
}



AttackBehaviour.cs

public abstract class AttackBehaviour : MonoBehaviour
{
// 개발자가 어떤 컴포넌트인지 확인할 수 있도록 주석 만들기.
#if UNITY_EDITOR
    [Multiline]
    public string developmentDescription = "";
#endif

    public int animationIndex;
    public int priority; // 여러 공격중 어떤 것을 선택할지에 대한 우선순위
    public int damage = 10;
    public float range = 3f;
    
    [SerializedField]
    protected float coolTime; // 공격후 대기 시간?
    protected float calcCoolTime; = 0.0f;
    
    public GameObject effectPrefab;
    
    [HideInInspector]
    public LayerMask targetMask;
    
    void Start()
    {
        calcCoolTime = coolTime; // 바로 공격 가능 상태
    }
    
    void Update()
    {
        if (calcCoolTime < coolTime)
        {
            calcCoolTime += Time.deltaTime;
        }
    }
    
    // startPoint는 발사체가 발사되는 지점용
    public abstract void ExecuteAttack(GameObject target = null, Transform startPoint = null);
}

 


EnemyController_New.cs

public EnemyController_New : EnemyController, IAttackable, IDamageable
{
    public Transform projectilePoint;
    
    [SerializedField]
    private List<AttackBehaviour> attackBehavious = new List<AttackBehaviour>();
    
    protected override void Start()
    {
        base.Start();
        stateMachine.AddState(new MoveState());
        stateMachine.AddState(new AttackState());
        stateMachine.AddState(new DeadState());
        
        InitAttackBehaviour();
    }
    
    protected override void Update()
    {
        CheckAttackBehaviour();
        base.Update();
    }
    
    private void InitAttackBehaviour()
    {
        foreach (AttackBehaviour behaviour in attackBehavious)
        {
            if (CurrentAttackBehaviour == null)
                CurrentAttackBehaviour = behaviour;
                
            behaviour.targetMask = TargetMask;
        }
    }
    
    private void CheckAttackBehaviour()
    {
        if (CurrentAttackBehaviour == null || !CurrentAttackBehaviour.IsAvailable)
        {
            CurrentAttackBehaviour = null;
            foreach (AttackBehaviour behaviour in attackBehavious)
            {
                if (behaviour.IsAvailable)
                {
                    // 가장 우선순위가 높은 공격 설정
                    if (CurrentAttackBehaviour == null || CurrentAttackBehaviour.priority < behaviour.priority)
                    {
                        CurrentAttackBehaviour = behaviour;
                    }
                }
            }
        }
    }
    
    public AttackBehaviour CurrentAttackBehaviour
    {
        get;
        private set; // 내부에서만 set 가능하도록..
    }
    
    public void OnExecuteAttack(int attackIndex)
    {
        if (CurrentAttackBehaviour != null && Target != null)
        {
            CurrentAttackBehaviour.ExecuteAttack(Target.gameObject, projectilePoint); // Target은 공격가능 거리내에 있는 타겟중 가장 가까운 것
        }
    }
}

 

 



적의 Animation 구동중에 OnCheckAttackCollider() 함수를 호출하도록 구현되어 있습니다.

캐릭터의 전투 시스템 구성은 C# 코딩으로 모두 이루어진다고 할 수 있겠네요.
이젠 배우는 내용들이 많아지므로 이전에 배운 내용은 코드 재사용을 하므로 정말 빠르게 진행됩니다.
이번에 배운 중요한 부분은 IAttackable이라고 봐야겠네요. 다음시간에는 IDamageable에 대해 구현하면 마무리가 될 것 같습니다.



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

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

 

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

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

www.fastcampus.co.kr

 

+ Recent posts