쯔꾸르식 유니티 게임 공부

30.몬스터 처치, 레밸 UP

ruripanda 2025. 3. 7. 13:47

using System.Collections;
using UnityEngine;

public class PlayerStat : MonoBehaviour
{
    public static PlayerStat instance;//인스턴스

    public int character_Lv;    //레벨
    public int[] needExp;
    public int currentEXP;      //현재 경험치

    public int hp;
    public int currentHP;//현재 체력
    public int mp;
    public int currentMP;//현재 마나

    public int atk;//공격력
    public int def;//방어력

    public string dmgSound;

    public GameObject prefabs_Flosting_text;//프리팹
    public GameObject parent;//부모 객체

    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {
        instance = this;//인스턴스
    }

    public void Hit(int _enemyAtk)
    {
        int dmg;

        if (def >= _enemyAtk)
            dmg = 1;
        else
            dmg = _enemyAtk - def;

        currentHP -= dmg;

        if (currentHP <= 0)
            Debug.Log("체력 0 미만, 게임오버");

        AudioManager.instance.Play(dmgSound);

        Vector3 vector = this.transform.position;
        vector.y += 60;

        GameObject clone = Instantiate(prefabs_Flosting_text, vector, Quaternion.Euler(Vector3.zero));
        clone.GetComponent<FloatingText>().text.text = dmg.ToString();
        clone.GetComponent<FloatingText>().text.color = Color.red;
        clone.GetComponent<FloatingText>().text.fontSize = 25;
        clone.transform.SetParent(parent.transform);
        StartCoroutine(HitCoroutine());
    }

    IEnumerator HitCoroutine()
    {
        Color color = GetComponent<SpriteRenderer>().color;
        color.a = 0;
        GetComponent<SpriteRenderer>().color = color;
        yield return new WaitForSeconds(0.1f);
        color.a = 1f;
        GetComponent<SpriteRenderer>().color = color;
        yield return new WaitForSeconds(0.1f);
        color.a = 0f;
        GetComponent<SpriteRenderer>().color = color;
        yield return new WaitForSeconds(0.1f);
        color.a = 1f;
        GetComponent<SpriteRenderer>().color = color;
        yield return new WaitForSeconds(0.1f);
        color.a = 0f;
        GetComponent<SpriteRenderer>().color = color;
        yield return new WaitForSeconds(0.1f);
        color.a = 1f;
        GetComponent<SpriteRenderer>().color = color;//이 부분작성을 빼놓지 말자 투명에서 유색으로 돌아오지 않는다
    }

    // Update is called once per frame
    void Update()
    {
        if(currentEXP >= needExp[character_Lv])//레벨업 함수
        {
            character_Lv++;
            hp += character_Lv * 2;
            mp += character_Lv + 2;

            currentHP = hp;
            currentMP = mp;
            atk++;
            def++;
        }
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerManager : MovingObjcet
{
    static public PlayerManager instance;

    //public string currentMapName;
    
    public string walkSound_1;
    public string walkSound_2;
    public string walkSound_3;
    public string walkSound_4;

    private AudioManager theAudio;

    //public AudioClip walkSound_1;//사운드 파일
    //public AudioClip walkSound_2;

    private AudioSource audioSource;//사운드 플레이어

    //뛰기,평범하게 걷기
    public float runSpeed;
    private float applyRunSpeed;
    private bool applyRunFlag = false;

    public bool notMove = false;//이벤트 실행시 움직임 정지
    private bool attacking = false;
    public float attackDelay;
    private float currentAttackDelay;

    //한칸한칸 이동할때 플레그 형식으로 구현
    private bool canMove = true;

    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {
        if (instance == null)
        {
            queue = new Queue<string>();
            DontDestroyOnLoad(this.gameObject);
            boxCollider = GetComponent<BoxCollider2D>();
            //audioSource = GetComponent<AudioSource>();
            animator = GetComponent<Animator>();
            theAudio = FindAnyObjectByType<AudioManager>();
            instance = this;
        }
        else
        {
            Destroy(this.gameObject);
        }
    }

    //코루틴으로 이동제어 한칸 한칸 이동하게
    IEnumerator MoveCoroutine()
    {
        while (Input.GetAxisRaw("Vertical") != 0 || Input.GetAxisRaw("Horizontal") != 0 && !notMove && !attacking)
        {
            //시프트 키 입력 여부에 따라 뛰기 걷기 구분
            if (Input.GetKey(KeyCode.LeftShift))
            {
                applyRunSpeed = runSpeed;
                applyRunFlag = true;
            }

            else
            {
                applyRunSpeed = 0;
                applyRunFlag = false;
            }

            //이동방향 벡터 움직임 구현
            vector.Set(Input.GetAxisRaw("Horizontal"), Input.GetAxisRaw("Vertical"), transform.position.z);

            //좌상,좌하 등 대각선 방향이동 장금
            if (vector.x != 0)
                vector.y = 0;

            //에니메이터 불러오기
            animator.SetFloat("DirX", vector.x);
            animator.SetFloat("DirY", vector.y);

            bool checkCollsionFlag = base.CheckCollsion();
            if (checkCollsionFlag)
                break;

            animator.SetBool("Walking", true);

            int temp = Random.Range(1, 4);
            switch (temp)
            {
                case 1:
                    theAudio.Play(walkSound_1);
                    break;
                case 2:
                    theAudio.Play(walkSound_2);
                    break;
                case 3:
                    theAudio.Play(walkSound_3);
                    break;
                case 4:
                    theAudio.Play(walkSound_4);
                    break;
            }
            theAudio.SetVolumn(walkSound_2, 0.5f);

            boxCollider.offset = new Vector2(vector.x * 0.02f * speed * walkCount, vector.y * 0.02f * speed * walkCount);//박스콜라이더를 미리 옮겨서 충돌방지

            //캐릭터 이동구현 코드
            while (currentWalkCount < walkCount)
            {
                if (vector.x != 0)
                {
                    //Translate는 현제 값에서 +를 해주는 코드
                    transform.Translate(vector.x * (speed + applyRunSpeed), 0, 0);
                }
                else if (vector.y != 0)
                {
                    transform.Translate(0, vector.y * (speed + applyRunSpeed), 0);
                }
                if (applyRunFlag)
                    currentWalkCount++;//위의 applyRunFlas가 되면 추가되는 카운트 만약 아니라면 더해지지 않음
                currentWalkCount++;//위의 카운트와는 별개의 카운트로 구분 됨
                if (currentWalkCount == 12)
                    boxCollider.offset = Vector2.zero;//박스콜라이더를 원위치하여 충돌방지
                yield return new WaitForSeconds(0.01f);//0.01초마다 대기

                /*if(currentWalkCount % 9 == 2)
                {
                    int temp = Random.Range(1, 2);
                    switch (temp)
                    {
                        case 1:
                            //audioSource.clip = walkSound_1;
                            //audioSource.Play();
                            break;
                        case 2:
                            //audioSource.clip = walkSound_2;
                            //audioSource.Play();
                            break;
                    }
                }*/

            }
            currentWalkCount = 0;//0을 넣어서 반복문 초기화

        }

        animator.SetBool("Walking", false);
        //한칸한칸 이동할때 bool값
        canMove = true;
    }

    //매 프레임마다 이동명령을 받음
    void Update()
    {
        if (canMove && !notMove && !attacking)
        {
            //이동 방향키 입력을 할때 쯔구르 처럼 상하좌우 구분하게 하는 코드
            if (Input.GetAxisRaw("Horizontal") != 0 || Input.GetAxisRaw("Vertical") != 0)
            {
                //한칸 한칸 이동할때 bool값으로 canMove를 이용함
                canMove = false;
                StartCoroutine(MoveCoroutine());
            }
        }
        if(!notMove && !attacking)
        {
            if (Input.GetKeyDown(KeyCode.Space))
            {
                currentAttackDelay = attackDelay;
                attacking = true;
                animator.SetBool("Attacking", true);
            }
        }
        if(attacking)
        {
            currentAttackDelay -= Time.deltaTime;
            if(currentAttackDelay <= 0)
            {
                animator.SetBool("Attacking", false);
                attacking = false;
            }
        }
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SlimeController : MovingObjcet//플레이어 캐릭터에 넣은 무빙오브젝트를 상속
{
    public float attackDelay;   //공격 딜레이(유예)

    public float inter_MoveWaitTime; //대기 시간(인스펙터에 표시)
    private float current_interMWT;  //실질적인 계산시간

    public string atkSound;          //어택사운드

    private Vector2 PlayerPos;       //플레이어의 좌표값(옆에 위에 있는지 확인)

    private int random_int;          //애너미가 랜덤으로 움직이기 위한 변수
    private string direction;        //UP,DOWN,RIGHT,LEFT 값

    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {
        queue = new Queue<string>();            //무빙오브젝트에 필요한 큐
        current_interMWT = inter_MoveWaitTime;  //시작하자마자 실질적 계산값 적용
    }

    // Update is called once per frame
    void Update()
    {
        current_interMWT -= Time.deltaTime;     //초당 -값으로 적용해서 시간이 지남을 적용

        if (current_interMWT <= 0)//0보다 작아지면 행동
        {
            current_interMWT = inter_MoveWaitTime;//작업 초기화

            if (NearPlayer())//NearPlayer이 참이라면
            {
                Flip();//플립으로 스프라이트 뒤집기 계산
                return;
            }

            RandomDirection();                  //랜덤하게 상하좌우 이동

            if (base.CheckCollsion())           //충돌체크
                return;                         //뭔가가 진로를 방해할경우 리턴으로 끊어준다

            base.Move(direction);               //무빙오브젝트 움직임
        }
    }

    private void Flip()//슬라임의 스프라이트를 뒤집어서 오른쪽 공격으로 만들어줌
    {
        Vector3 flip = transform.localScale;    //백터 플립이라는 변수를 만들어주고 스케일을 대입해줌
        if (PlayerPos.x > this.transform.position.x)//플레이어 포인트.x의 위치가 클 경우
            flip.x = -1;                        //스프라이트를 뒤집어줌
        else
            flip.x = 1f;                        //아니니라면 정상적으로 오른쪽 공격을 실행
        this.transform.localScale = flip;       //현재 스케일에 계산한 플립을 대입
        animator.SetTrigger("Attack");          //어택실행
        StartCoroutine(WaitCoroutine());        //코루틴 waitCoroutine를 실행
    }

    IEnumerator WaitCoroutine()
    {
        yield return new WaitForSeconds(attackDelay);
        AudioManager.instance.Play(atkSound);
        if (NearPlayer())//리턴이 true가 된다면 실행
            PlayerStat.instance.Hit(GetComponent<EnemyStat>().atk);
    }

    private bool NearPlayer()//플레이어가 근처에 있는지 조건을 확인
    {
        PlayerPos = PlayerManager.instance.transform.position;//플레이어 위치를 받아옴

        //MAthf.Abs는 설대값을 반환하는 함수이다 절대값은 음수가 나오면 정수로 바꿔주고 정수는 정수로 출력한다 즉 벡터가 -가 나와도 계산이 꼬일 일이 없다

        if (Mathf.Abs(Mathf.Abs(PlayerPos.x) - Mathf.Abs(this.transform.position.x)) <= speed * walkCount)
            //가로의 위치 측정 (플레이어포지션.x - 자신의 포지션을 해주고 덤으로 스피드와 워크카운트를 곱한 값을 비교해서 작거나 같을 경우
        {
            if (Mathf.Abs(Mathf.Abs(PlayerPos.y) - Mathf.Abs(this.transform.position.y)) <= speed * walkCount * 0.5f)
                //여기는 세로 플레이어포지션.y - 자신의 포지션.y 그뒤 스피드와 워크카운트 곱하고 절반분의 값을 계산뒤 이 값이 크거나 같을 경우
            {
                return true;//리턴 true로 끊어줌
            }
        }
        if (Mathf.Abs(Mathf.Abs(PlayerPos.y) - Mathf.Abs(this.transform.position.y)) <= speed * walkCount)//이 부분은 세로와 가로를 반대로 측정해줌
        {
            if (Mathf.Abs(Mathf.Abs(PlayerPos.x) - Mathf.Abs(this.transform.position.x)) <= speed * walkCount * 0.5f)
            {
                return true;
            }
        }

        return false;
        //위 두가지 조건이 만족하지 않으면 플레이어가 없으므로 false로 거짓으로 리턴한다
    }

    private void RandomDirection()      // 랜덤하게 움직이는 함수
    {
        vector.Set(0, 0, vector.z);     //백터값을 초기화
        random_int = Random.Range(0, 4);//0~3개의 난수 설정
        switch (random_int)             //스위치 문으로 랜덤한 값에 따라 움직임을 적용
        {
            case 0:
                vector.y = 1f;
                direction = "UP";//위로이동
                break;
            case 1:
                vector.y = -1f;
                direction = "DOWN";//아래로 이동
                break;
            case 2:
                vector.x = 1f;
                direction = "RIGHT";//오른쪽으로 이동
                break;
            case 3:
                vector.x = -1f;
                direction = "LEFT";//왼쪽으로 이동
                break;
        }
    }
}
using UnityEngine;

public class EnemyStat : MonoBehaviour
{
    public int hp;
    public int currentHp;//현제 HP
    public int atk;//공격
    public int def;//방어
    public int exp;//경험치

    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {
        currentHp = hp;//초기 HP세팅
    }

    public int Hit(int _playerAtk)          //맞았을 경우 함수
    {
        int playerAtk = _playerAtk;         //들어오는 데미지
        int dmg;//실제 적용 데미지
        if (def >= playerAtk)               //데미지가 방어력과 같거나 적을 경우
            dmg = 1;                        //0이 안나오고 1만 적용
        else
            dmg = playerAtk - def;          //더 높을 경우 들어가는 데미지

        currentHp -= dmg;                   //현제 HP에 데미지를 뺌

        if(currentHp <= 0)                  //현제 HP가 0보다 낮거나 같을 경우
        {
            Destroy(this.gameObject);       //자신을 삭제하고
            PlayerStat.instance.currentEXP += exp;//플레이어스텟 인스턴스의 exp에 증감
        }

        return dmg;                         //데미지를 리턴
        
    }
}
using UnityEngine;

public class HurtEnemy : MonoBehaviour
{
    public GameObject prefabs_Floating_Text;
    public GameObject parent;

    public string atkSound;

    private PlayerStat thePlayerStat;

    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {
        thePlayerStat = FindAnyObjectByType<PlayerStat>();
    }

    private void OnTriggerEnter2D(Collider2D collision)
    {
        if(collision.gameObject.tag == "enemy")
        {
            int dmg = collision.gameObject.GetComponent<EnemyStat>().Hit(thePlayerStat.atk);
            AudioManager.instance.Play(atkSound);

            Vector3 vector = collision.transform.position;
            vector.y += 60;

            GameObject clone = Instantiate(prefabs_Floating_Text, vector, Quaternion.Euler(Vector3.zero));
            clone.GetComponent<FloatingText>().text.text = dmg.ToString();
            clone.GetComponent<FloatingText>().text.color = Color.white;
            clone.GetComponent<FloatingText>().text.fontSize = 25;
            clone.transform.SetParent(parent.transform);
        }
    }
}

 

이번시간에는 몬스터 처치와 레밸업을 구현하겠다

일단 몬스터의 스텟을 관리하게 SlimeController스크립트를 켜주자

 

빨갓게 표시한 변수는 이제 쓰지 않는다

없세거나 주석처리해주자

 

여기도 체크한 부분은 지우거나 주석처리해주자

 

이제 슬라임의 스텟을 관리하게 EnemyStat를 작성해주자

주석으로 설명이 가능해서 주석으로 설명을 체워넣었다

 

이제 enemy가 데미지를 입는 스크립트를 만든다 HurtEnemy 라고 이름을 지어 C# 스크립트를 만들자

 

EnemyStat 스크립트다 주석으로 설명이 충분히 가능해서 주석으로 설명을 넣었다

boxcollider2D 콜라이더에 감지가 되면 데미지가 들어가는 것을 작성한 스크립트다

 

이제 적을 때리는 이팩트를 적용해보자

유니티 엔진으로 들어가자

 

 

플레이어의 자식 오브젝트로 Attack_small오브젝트를 Hierarchy에 넣어주자

 

그리고 오브젝트 이름옆의 체크박스를 해제하여 평소에는 안보이게 한다

이 오브젝트는 우리가 공격을 했을때만 활성화하여 보이게 한다

 

이제 우리는 보는 방향대로 공격하는 스프라이트를 만들을 것이다

 

처음에는 Character_Attack_Down을 만들어 주자

캐릭터를 아래로 보는 상태에서 녹화버튼을 누르고 2번째 스샷의 Attack_Small을 활성화하여 캐릭터 앞에 붙여준다

녹화버튼을 이제 비활성화하여 꺼준다

 

우리는 캐릭터의 공격에 1프레임만 쓸 것이기 때문에 1프레임에 이렇게 넣을 것이다

이제 필자가 가르쳐 준 것을 참고하여 Up,Left,Right를 만들어주자

 

참고로 Attack_Small 오브젝트의 방향은 Rotation을 만져줘서 반대로 만들 수도 있다 참고하자

 

이제 Animator로 들어가서 새로 만들은 애니메이션 작동을 손봐주자

일단 trigger타입으로 Attacking를 만들어 준다

 

 

새로 만들은 어텍 노드는 전부 지워주고

Blend Tree로 블렌드 트리를 만들어준다

 

블렌드 트리의 Base Layer이다 참고해서 만들어주자

 

그뒤 블렌드 트리의 박스를 더블 클릭해서 들어간다

블렌드 트리는 이렇게 표를 참고하여 작성해주자

-1은 다운 1은 업이라서 외우기가 쉬운데 -1리 Left, 1이 Right인건 좀 외워두자 필자도 자주 까먹는다

 

그뒤 블렌드 트리 내부의 애니메이션 박스를 더블 클릭해서 Loop Time를 비활성화해주자

이건 반복이라고 지난번에도 말했다 이거 필히 기억해야 한다

 

이제 어텍 매니져를 사용하기위해 PlayerManager C#스크립트를 수정한다

 

Attack 기능을 넣기위해 변수 3개를 이렇게 선언해준다

bool 타입으로 attacking = false로 선언해주고

float 타입으로 attackDelay로 딜레이를 선언해주고

float 타입으로 currentAttackDelay를 선언해준다

 

 

그리고 우리가 코루틴으로 이동 로직을 작성할때 !notMove를 해준적이 있다 여기에 !attacking를 넣어주어 공격중에는 이동을 하지 못하게 조건문을 추가해준다

어택은 늘 실행되야 하므로 void Update에 작성한다

위에 이동로직에도 !attacking를 잊지말고 작성해준다

 

168번에는 !notMove와 !attacking를 조건문으로 걸고 해당 로직이 실행되지 않을때

키보드 스페이스 버튼을 클릭하면 어텍딜레이가 적용되고

attacking가 true가 되면서 animator.SetBool("Attacking", true)로 이번에 새로 작성한 애니매이션이 실행되게 한다

 

177번째 줄은 attacking가 true 일때

currentAttackDelay에 -=Time.deltaTime로 현실 초를 빼주면서

조건문 0보다 같거나 작게되면

SetBool("Attacking",false)로 공격 애니메이션을 종료해주고

attacking를 false로 만들어 준다

 

이제 유니티엔진으로 돌아간다

 

일단 아까 공격 이펙트에 BoxCollider2D를 만들어준다

이게 적당한 크기가 되어야지 공격이 먹혀드므로 크기를 주의하자

 

그리고 우리가 작성해준 Hurt Enemy 스크립트도 붙여준다

Prefabs_FloatingT...은 지난번에 만들어준 프리팹을 넣어주고

Parent 에는 Canvas를 넣어준다

Atk Sound에는 사운드를 적당한 것을 넣어준다

 

그리고 PlayerManager에 Attack Delay가 추가되었는데 0.4정도로 하자

 

슬라임도 손보자

Tag를 enemy로 바꿔주고

AttackDelay도 0.6으로 넣어주고 EnemhyStat도 hp 15, atk 5, def 1 , exp 10을 주자

 

그리고 PlayerStat도 손봐주자

hp50, mp 0 , atk 5, def 0 쯤으로 해주자

 

이제 레벨업을 구현해보자

PlayerStat 스크립트를 열어서 아래 코드를 추가하자

 

83번째 줄은 currentExp가 needExp[character_Lv] 보다 크거나 같을 경우 업데이트에서

캐릭터 레벨을 증감시켜주고

hp는 *2를 해주고 mp는 2 증가시켜준다

 

그뒤 89와 90번줄에 바로 적용시켜주고 atk와 def에 1씩 증감시켜준다

 

그러면 이렇게 레벨업 경험치 총량이 뜨고 우리가 만들은 몬스터를 경험치만큼 잡아주면 레벨업이 된다

'쯔꾸르식 유니티 게임 공부' 카테고리의 다른 글

26.a 오타 수정  (0) 2025.03.08
31.이팩트  (0) 2025.03.07
29.피격효과 스텟구현  (0) 2025.03.05
28.슬라임(몬스터) 구현  (0) 2025.02.28
27.플로팅 텍스트(아이템 습득시 이름 표시)  (0) 2025.02.27