Print Friendly and PDF

유니티/게임 제작

[디펜스 게임 제작] 몬스터 제작

나는야 개발자 2025. 6. 10. 23:26
반응형

사용 패턴 및 구조

- 템플릿 매서드 패턴 및 상속 계층 구조

- ScriptableObject로 메모리 절약 유도

 

구조 설명

1. 추상 클래스로 공통적으로 사용할 것들을 모우는 BaseNPC 제작

2. 몬스터에게도 여러 종류가 존재할 것이기 때문에 Monster_Base에 BaseNPC를 상속

3. 각 종류별 몬스터에게 Monster_Base를 상속시켜 필요한 부분 제작

 

구조 제작 의도

- 상속을 이용해 중복 코드 최소화 및 확장성을 고려한 작업 진행

 



애니메이션 리소스 : 바로가기

 

Enemy Galore 1 - Pixel Art | 2D 캐릭터 | Unity Asset Store

Elevate your workflow with the Enemy Galore 1 - Pixel Art asset from Admurin. Find this & more 캐릭터 on the Unity Asset Store.

assetstore.unity.com

 

- 하나를 지정해서 애니메이션 적용

 

- hp표기용 hpbar 이미지 추가, Canvas Scaler을 화면 크기에 맞게 조정

 

HP바 추가

using UnityEngine;

public class UI_Follows_Object : MonoBehaviour
{
    [SerializeField] RectTransform _myui;
    [SerializeField] Transform _targetobject;
    [SerializeField] Vector3 _offset;
    Camera _maincamera;

    void Start()
    {
        _maincamera = Camera.main;
    }

    void LateUpdate()
    {
        if (_targetobject == null || _maincamera == null)
        {
            return;
        }

        _myui.position = _maincamera.WorldToScreenPoint(_targetobject.position) + _offset;
    }
}

- UI가 오브젝트를 따라다닐 수 있게 컴퍼논트 추가

 

공용으로 사용할 Scriptable Object추가 

using UnityEngine;

[CreateAssetMenu(fileName = "SO_NPC", menuName = "SO_NPC", order = 0)]
public class SO_NPC : ScriptableObject
{
    public int _Hp;
    public int _Damge;
    public int _Armor;
    public float _Critical;// 0~1
    public float _Critical_damage; // 0~1

    public int TotalDamage()
    {
        float damage = _Damge;

        var critical_random_value = UnityEngine.Random.Range(0f, 1f);
        if (critical_random_value <= _Critical)
        {
            damage = damage * (_Critical_damage + 1);
        }

        return Mathf.FloorToInt(damage);
    }
}

- 기본적으로 사용될 hp, damage, armor, critical, critical_damage 맴버변수 정의

- 모든 npc가 동일하게 사용할 데미지 불러오기를 ScriptableObject에 정의

 

공용 NPC 클래스 추가

using System;
using UnityEngine;

public abstract class BaseNPC : MonoBehaviour
{
    //NPC 별 데이터 
    [SerializeField] SO_NPC _so_npc;
    [SerializeField] AnimationController _animationController;

    //기본 맴버변수 
    protected int _current_hp;

    //이벤트 변수들
    public event Action _die_event;
    public event Action _hit_event;
    public event Action _attack_event;

    //함수
    protected virtual void Start()
    {
        ReSetting();
        _die_event += () => PlayAnimation(EANIMATION.DIE);
        _hit_event += () => PlayAnimation(EANIMATION.HIT);
        _attack_event += () => PlayAnimation(EANIMATION.ATTACK);
    }

    protected virtual void ReSetting()
    {
        _current_hp = _so_npc._Hp;
    }

    protected virtual void Target_To_Attack(BaseNPC target_npc)
    {
        var my_damage = _so_npc.TotalDamage();
        target_npc.Hp_Update(my_damage);
        _attack_event?.Invoke();
    }

    protected virtual void Hp_Update(int target_damage)
    {
        _current_hp -= target_damage;
        _hit_event?.Invoke();

        if (_current_hp <= 0)
        {
            NPC_Die();
        }
    }

    protected virtual void NPC_Die()
    {
        _die_event?.Invoke();
    }

    protected virtual void PlayAnimation(EANIMATION eanimation)
    {
        _animationController.PlayAnimation(eanimation);
    }
}

- event action을 이용해 캡슐화 및 확장성 있도록 구현

- 각 함수들에 virtual로 중복코드 최소화

 

공용 몬스터 클래스 추가 

using System;
using UnityEngine;

public class Monster_Base : BaseNPC
{
    [SerializeField] MoveController _moveController;

    protected override void Start()
    {
        _moveController._move_event += () => PlayAnimation(EANIMATION.RUN);
    }

    public void OnRelease()
    {
        this.gameObject.SetActive(false);
        _moveController.ReSetting();
    }

    public void OnSpawn()
    {
        this.gameObject.SetActive(true);
    }
}

- 풀링을 이용해 몬스터를 생성할 것이기 때문에 OnSpanw, OnRelease, Create를 이용해 기본적으로 사용될 몬스터 코드 정의

 

기본 몬스터 클래스 추가

using UnityEngine;

public class Monster_Normal : Monster_Base
{

}

- 각 몬스터 종류별로 처리가 다를 것이기 때문에 Monster_Base를 상속한 클래스 추가

- 별도 처리가 필요하면 추가 예정

 

이동 클래스 추가

using System;
using UnityEngine;

public class MoveController : MonoBehaviour
{
    public event Action _move_event;
    [SerializeField] float _speed;

    Vector2? _targetPosition = null;

    void FixedUpdate()
    {
        if (!_targetPosition.HasValue)
        {
            return;
        }
        MoveToUpdate();
    }

    public virtual void MoveToTarget(Vector2 target)
    {
        _targetPosition = target;
        _move_event?.Invoke();
    }
    
    public void ReSetting()
    {
        _targetPosition = null;
    }

    void MoveToUpdate()
    {
        Vector2 currentPosition = transform.position;
        Vector2 target = _targetPosition.Value;
        if (Vector2.Distance(currentPosition, target) > 0.01f)
        {
            Vector2 newPosition = Vector2.MoveTowards(currentPosition, target, _speed * Time.fixedDeltaTime);
            transform.position = new Vector3(newPosition.x, newPosition.y, transform.position.z);
        }
        else
        {
            transform.position = new Vector3(target.x, target.y, transform.position.z);
            _targetPosition = null;
        }
    }
}

- 이동하는 NPC에게만 달아주기 위한 MoveController추가

 

애니메이션 클래스 추가

using UnityEngine;

public class AnimationController : MonoBehaviour
{
    [SerializeField] Animator _animator;
    public void PlayAnimation(EANIMATION eanimationkind)
    {
        _animator.SetTrigger(eanimationkind.ToString());
    }
}

public enum EANIMATION
{
    IDLE,
    RUN,
    HIT,
    DIE,
    ATTACK
}

- 애니메이션 실행용 AnimationController추가

 

몬스터 프리팹 설정

- 몬스터 컴포넌트를 비롯해 Move, Hpbar, Animation등 각종 필요한 컴포넌트 추가

- Monster Layer추가 및 BoxCollder2D, RigidBody2D 추가

 

Move애니메이션에서만 움직이도록 이벤트 추가 

public class MoveController : MonoBehaviour
{
    ...이하생략
    public event Action _move_end_check;
	public event Func<bool> _move_check;
    ...이하생략
    
     void FixedUpdate()
    {
        if (!_targetPosition.HasValue)
        {
            return;
        }
        if (_move_check == null ? true : _move_check.Invoke())
        {
            return;
        }

        MoveToUpdate();
    }
    ...이하생략
    
     void MoveToUpdate()
    {
        Vector2 currentPosition = transform.position;
        Vector2 target = _targetPosition.Value;
        if (Vector2.Distance(currentPosition, target) > 0.01f)
        {
            Vector2 newPosition = Vector2.MoveTowards(currentPosition, target, _speed * Time.fixedDeltaTime);
            transform.position = new Vector3(newPosition.x, newPosition.y, transform.position.z);
        }
        else
        {
            transform.position = new Vector3(target.x, target.y, transform.position.z);
            _targetPosition = null;
            _move_end_check?.Invoke();
        }
    }
}

- public event Action _move_end_check 를 추가하여 움직임이 멈췄을때 이벤트 추가 

- public event Func<bool> _move_check 를 추가하여 움직임 여부 이벤트 추가 

 

using System;
using UnityEngine;

public class Monster_Base : BaseNPC
{
    [SerializeField] MoveController _moveController;

    protected override void Start()
    {
        PlayAnimation(EANIMATION.IDLE);
        _moveController._move_event += () => PlayAnimation(EANIMATION.MOVE, true);
        _moveController._move_end_check += () => PlayAnimation(EANIMATION.MOVE, false);
        _moveController._move_check += () => _animationController.CheckRunAnimation();
    }

    public void OnRelease()
    {
        this.gameObject.SetActive(false);
        _moveController.ReSetting();
    }

    public void OnSpawn(Vector2 target)
    {
        this.gameObject.SetActive(true);
        _moveController.MoveToTarget(target);
    }
}

- MonsterBase 클래스에서 이동종료, 이동 체크 이벤트 추가

- OnSpawn할때 이동 타겟 바로 지정해주기

 

public class AnimationController : MonoBehaviour
{
    [SerializeField] Animator _animator;
    EANIMATION _eanimation;

    public void PlayAnimation(EANIMATION eanimation)
    {
        _animator.SetTrigger(eanimation.ToString());
    }

    public void PlayAnimation(EANIMATION eanimation, bool isaction)
    {
        _animator.SetBool(eanimation.ToString(), isaction);
    }

    public bool CheckRunAnimation()
    {
        return _eanimation == EANIMATION.MOVE;
    }

    /// <summary>
    /// 애니메이션 이벤트
    /// </summary>
    public void SetAnimatorEvent(EANIMATION eanimation)
    {
        _eanimation = eanimation;
    }
}

- MoveController에 _move_check이벤트 추가 및 FixedUpdate에 체크하기

- SetAnimatorEvent를 이용해 애니메이션 재생 시 현재 애니메이션 상태를 저장한다.

 

using UnityEngine;

public class AnimationEvent : StateMachineBehaviour
{
    [SerializeField] EANIMATION _eanimation;
    
    // AnimationController 캐싱용
    private AnimationController _controller;
    
    // 상태 진입 시 호출
    public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        // 첫 번째 호출 시에만 AnimationController 캐싱
        if (_controller == null)
        {
            _controller = animator.GetComponent<AnimationController>();
        }
        
        if (_controller == null)
            return;

        // Inspector에서 설정한 애니메이션 타입으로 SetAnimatorEvent 호출
        _controller.SetAnimatorEvent(_eanimation);
    }
}

- Add Behaviour을 이용해 이벤트 추가 및 어떤 상태인지 지정

 

 

*공격과 데미지 닳는거 및 테스트는 다른 것을 만들고 난 후 추가 및 확인 예정


작업 시간

1차 - 6/10 22:40~23:20 

2차 - 6/11 21:00~21:40 - BaseNPC(수정), BaseMonster(수정), MoveController(추가), AnimationController(추가)

3차 - 6/11 22:20~23:20 - Enemy Galore 1 - Pixel Art(리소스 추가), Monster_(프리팹 추가) 및 애니메이션 적용

4차 - 6/12 22:30~23:30 - UI_Follow_Ojbect(추가), Monster_ -> Monster_Bat(이름 변경 및 컴포넌트 추가), Hpbar(추가)

5차 - 6/13 21:00 ~ 21:40 / 6/14 11:20~12:40 - AnimationEvent(추가), AnimationController,MoveController,BaseMonster(수정) , Bat Animato(수정)

반응형