티스토리 뷰

금일 한 것

▶ 에너미 시스템 추가

  • 이동 로직(플레이어 추적) 구현
  • 공격 기능 구현
  • 피격과 이로 인한 사망 기능 구현
  • 자동 생성 기능 추가
    • 플레이어 근처에서는 생성되지 않음.
  • 등급(미니언, 엘리트, 챔피언) 구현
    • 엘리트: 미니언의 강화 버전으로 작성. 구간 별 보스 역할.
    • 챔피언: 별개 패턴을 가진 유닛으로 구성. 최종 보스 역할.

 

※ 몬스터 및 플레이어의 스텟에 따른 차이점은 현재 생명력(Life) 이외에는 전무합니다.

     ┗ 추후 스킬 및 공격 관련 요소 모듈화 작업을 진행할 때에 스텟의 효과를 받도록 구성할 예정입니다.

     ┗ 또한, 같은 시기에 플레이어의 다른 능력치도 효력이 있게끔 구성할 예정입니다.

 

결과물(영상)



 

코드(일부)

에너미 - 부모 클래스 [겸 임시 챔피언 클래스]

더보기
[CreateAssetMenu(menuName = "Scriptables/Enemy/Base")]
public class Enemy_Base : ScriptableObject
{
    public int Life_Max;

    public int Atk;
    public int Dec;

    // 내성 [상흔 / 중독 / 심약]
    public int[] Sta_Imm = new int[3];

    // 상세 능력치
    // ※ [피해량 = {공격력 * (100 + Atk_Mul) / (100 + Res_Mul) } - Dec]로 판정된다.
    public int[] Res_Mul = new int[5]; // 피해 감소 보정치. [물리 / 화염 / 번개 / 신성 / 신비]

    public float velocity = 0.04f;
    protected float refeat = 0.05f;

    public GameObject Projectile_Pref;
    public GameObject Pulse_Pref;
    public GameObject Orb_Pref;

    public virtual void Patton(ref float timer, ref int count, GameObject Self_Unit)
    {
        GameObject Target_Unit = GameManager.Instance.Player_Unit;
        int[] Damage = { 4, 0, 0, 0, 0 };

        if (timer >= refeat)
        {
            if (count % 5 == 0)
                Emit_Pulse(Self_Unit.transform.position, 5.0f, 2.5f, Damage);

            if (count % 20 < 8)
            {
                Shoot_Proj(Self_Unit.transform.position, count * 15.0f, 0.05f, 1.5f, Damage);
                Shoot_Proj(Self_Unit.transform.position, 120.0f + (count * 15.0f), 0.05f, 1.5f, Damage);
                Shoot_Proj(Self_Unit.transform.position, -120.0f + (count * 15.0f), 0.05f, 1.5f, Damage);
            }

            if(count % 35 < 5 && count % 2 == 0)
            {
                float direction = Mathf.Atan2(Target_Unit.transform.position.y - Self_Unit.transform.position.y, Target_Unit.transform.position.x - Self_Unit.transform.position.x) * Mathf.Rad2Deg;
                float distance = Vector3.Distance(Self_Unit.transform.position, Target_Unit.transform.position);

                if (distance > 3.0f)
                    Shoot_Proj(Self_Unit.transform.position, direction, 0.1f, 3.5f, Damage);
            }

            timer -= refeat;
            
            if (count >= 20)
                count = 0;
            else
                count++;
        }
    }

    public virtual void Move_Chara(float _velocity, GameObject Self_Unit)
    {
        GameObject Target_Unit = GameManager.Instance.Player_Unit;
        // 탄젠트 값을 이용하여 Radian 각도를 구한 뒤 이를 Degree 각도로 치환하는 공식.
        float Target_Degree = Mathf.Atan2(Target_Unit.transform.position.y - Self_Unit.transform.position.y, Target_Unit.transform.position.x - Self_Unit.transform.position.x) * Mathf.Rad2Deg;
        // 두 점 간의 거리를 구하는 함수.
        float distance = Vector3.Distance(Self_Unit.transform.position, Target_Unit.transform.position);

        Self_Unit.transform.position += Quaternion.Euler(0.0f, 0.0f, Target_Degree) * Vector3.right * _velocity;
    }

    public void Shoot_Proj(Vector3 _Position, float _rotation, float _velocity, float _Size, int[] _Damage)
    {
        GameObject temp = Instantiate(Projectile_Pref, _Position, Quaternion.identity);
        temp.SetActive(false);

        Projectile temp_Proj = temp.GetComponent<Projectile>();
        temp_Proj.is_friendly = false;
        temp_Proj.velocity = _velocity;
        temp_Proj.rotate = _rotation;
        temp_Proj.Fork.Add(3);
        temp_Proj.Damage = _Damage;
        temp_Proj.Size = new Vector3(_Size, _Size, 1.0f);

        temp.SetActive(true);
    }

    public void Emit_Pulse(Vector3 _Position, float E_Speed, float _Size, int[] _Damage)
    {
        GameObject temp = Instantiate(Pulse_Pref, _Position, Quaternion.identity);
        temp.SetActive(false);

        Pulse temp_Pul = temp.GetComponent<Pulse>();
        temp_Pul.is_friendly = false;
        temp_Pul.speed = E_Speed;
        temp_Pul.Damage = _Damage;
        temp_Pul.Size = new Vector3(_Size, _Size, 1.0f);

        temp.SetActive(true);
    }

    public void Create_Orb(Vector3 _Position, float _Time_Max, float _Time_Term, float _Size, float _velocity, float _Direction, int[] _Damage)
    {
        GameObject temp = Instantiate(Orb_Pref, _Position, Quaternion.identity);
        temp.SetActive(false);

        Orb temp_Orb = temp.GetComponent<Orb>();
        temp_Orb.is_friendly = false;
        temp_Orb.Time_Max = _Time_Max;
        temp_Orb.TIme_Term = _Time_Term;
        temp_Orb.velocity = _velocity;
        temp_Orb.Direction = _Direction;
        temp_Orb.Damage = _Damage;
        temp_Orb.Size = new Vector3(_Size, _Size, 1.0f);

        temp.SetActive(true);
    }
}

 

미니언 & 엘리트

더보기
[CreateAssetMenu(menuName = "Scriptables/Enemy/Minion_1")]
public class Minion_1 : Enemy_Base
{
    public override void Patton(ref float timer, ref int count, GameObject Self_Unit)
    {
        GameObject Target_Unit = GameManager.Instance.Player_Unit;
        int[] Damage = { 4, 0, 0, 0, 0 };

        float direction = Mathf.Atan2(Target_Unit.transform.position.y - Self_Unit.transform.position.y, Target_Unit.transform.position.x - Self_Unit.transform.position.x) * Mathf.Rad2Deg;
        float distance = Vector3.Distance(Self_Unit.transform.position, Target_Unit.transform.position);

        if (timer >= refeat)
        {
            if (count >= 50)
                count = 0;

            if (distance < 1.0f)
            {
                if (count == 0)
                    Emit_Pulse(Self_Unit.transform.position, 2.5f, 2.5f, Damage);
                
                count++;
            }

            else if (count != 0)
                count++;
        }
    }

    public override void Move_Chara(float _velocity, GameObject Self_Unit)
    {
        GameObject Target_Unit = GameManager.Instance.Player_Unit;

        // 탄젠트 값을 이용하여 Radian 각도를 구한 뒤 이를 Degree 각도로 치환하는 공식.
        float Target_Degree = Mathf.Atan2(Target_Unit.transform.position.y - Self_Unit.transform.position.y, Target_Unit.transform.position.x - Self_Unit.transform.position.x) * Mathf.Rad2Deg;
        // 두 점 간의 거리를 구하는 함수.
        float distance = Vector3.Distance(Self_Unit.transform.position, Target_Unit.transform.position);

        Self_Unit.transform.position += Quaternion.Euler(0.0f, 0.0f, Target_Degree) * Vector3.right * _velocity;
    }
}

자동 생성 코드

더보기
void Update()
{
    for (int i = 0; i < Timer.Length; i++)
    {
        Timer[i] += Time.deltaTime;
    }

    if (Timer[1] >= 1.0f)
    {
        Create_Minion();
        Timer[1] = 0.0f;
    }

    if (Timer[2] >= 5.0f) // 시간은 원래 180.0초로 둬야 하나, 테스트용이라 5초로 설정.
    {
        if (count < 2)
        {
            Create_Champion();
            Timer[2] = 0.0f;
            count++;
        }

        else if (count == 2)
        {
            Create_Chieftain();
            Timer[2] = 0.0f;
            count++;
        }
    }
}

void Create_Minion()
{
    float Rand_x_Pos = Player_Unit.transform.position.x;
    float Rand_y_Pos = Player_Unit.transform.position.y;

    if (Random.Range(0.0f, 1.0f) > 0.5f)
        Rand_x_Pos += Random.Range(2.5f, 8.0f);
    else
        Rand_x_Pos -= Random.Range(2.5f, 8.0f);

    if (Random.Range(0.0f, 1.0f) > 0.5f)
        Rand_y_Pos += Random.Range(2.5f, 8.0f);
    else
        Rand_y_Pos -= Random.Range(2.5f, 8.0f);

    Instantiate(Minion_List[Random.Range(0, Minion_List.Count)], new Vector3(Rand_x_Pos, Rand_y_Pos, 0.0f), Quaternion.identity);
}

void Create_Champion()
{
    float Rand_x_Pos = Player_Unit.transform.position.x;
    float Rand_y_Pos = Player_Unit.transform.position.y;

    if (Random.Range(0.0f, 1.0f) > 0.5f)
        Rand_x_Pos += Random.Range(2.5f, 8.0f);
    else
        Rand_x_Pos -= Random.Range(2.5f, 8.0f);

    if (Random.Range(0.0f, 1.0f) > 0.5f)
        Rand_y_Pos += Random.Range(2.5f, 8.0f);
    else
        Rand_y_Pos -= Random.Range(2.5f, 8.0f);

    GameObject Unit = Instantiate(Minion_List[Random.Range(0, Minion_List.Count)], new Vector3(Rand_x_Pos, Rand_y_Pos, 0.0f), Quaternion.identity);
    Unit.SetActive(false);
    Unit.GetComponent<Enemy_Cell>().is_Elite = true;
    Unit.SetActive(true);
}

void Create_Chieftain()
{
    float Rand_x_Pos = Player_Unit.transform.position.x;
    float Rand_y_Pos = Player_Unit.transform.position.y;

    if (Random.Range(0.0f, 1.0f) > 0.5f)
        Rand_x_Pos += Random.Range(2.5f, 8.0f);
    else
        Rand_x_Pos -= Random.Range(2.5f, 8.0f);

    if (Random.Range(0.0f, 1.0f) > 0.5f)
        Rand_y_Pos += Random.Range(2.5f, 8.0f);
    else
        Rand_y_Pos -= Random.Range(2.5f, 8.0f);

    GameObject Unit = Instantiate(Champion_List[Random.Range(0, Minion_List.Count)], new Vector3(Rand_x_Pos, Rand_y_Pos, 0.0f), Quaternion.identity);
    Unit.SetActive(false);
    Unit.GetComponent<Enemy_Cell>().is_Elite = true;
    Unit.GetComponent<Enemy_Cell>().is_Champion = true;
    Unit.SetActive(true);
}

 

마무리하며

전체적으로 클래스 간의 부모-자식 관계를 어떻게 구성할지를 고민하느라 시간이 오래 걸린 파트입니다.

결과적으로는 부모 쪽에서 거의 모든 요소를 포함하되, 공격과 이동 함수는 몬스터의 종류에 따라서 변화할 수 있도록 virtual - override 구성으로 작성하는 식으로 제작했습니다.

일단 적 사망 시에는 아무런 효과 없이 뿅 하고 없어진다는 점이나, 가시성을 위해서 충돌이 아니라 파동으로 공격하도록 구성했거나 하는 등, 추후 추가 작업을 해야하는 요소들이 이래저래 존재하긴 하지만, 일단 최소 기능으로는 완성했다고 봐도 될 거 같아요.

 

이번 작업을 하면서 "생각해 보니 데미지 숫자 표시 기능을 안넣었구나..." 싶었습니다.

일단 아마 간단하게 끝날 거 같긴 해서, 조만간 간단하게 작업해야겠네요.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
글 보관함