기록 보관소

[Unity/유니티] 기초-뱀서라이크: 타격감 있는 몬스터 처치 만들기[09] 본문

유니티 프로젝트/뱀서라이크

[Unity/유니티] 기초-뱀서라이크: 타격감 있는 몬스터 처치 만들기[09]

JongHoon 2023. 7. 10. 20:22

개요

유니티 독학을 위해 아래 링크의 골드메탈님의 영상들을 보고 직접 따라 해보면서 진행 상황을 쓰고 배웠던 점을 요약한다.

https://youtube.com/playlist?list=PLO-mt5Iu5TeYI4dbYwWP8JqZMC9iuUIW2 

 

📚유니티 기초 강좌

유니티 게임 개발을 배우고 싶은 분들을 위한 기초 강좌

www.youtube.com


뱀서라이크: 타격감 있는 몬스터 처치 만들기[09]

1. 피격 리액션

//Enemy Script

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Enemy : MonoBehaviour {
    public RuntimeAnimatorController[] animCon;
    public float health;
    public float maxHealth;
    public float speed;
    public Rigidbody2D target;  //쫓아갈 타겟(플레이어)

    bool isLive;

    Animator anim;
    Rigidbody2D rigid;
    SpriteRenderer spriter;
    WaitForFixedUpdate wait;

    void Awake() {
        anim = GetComponent<Animator>();
        rigid = GetComponent<Rigidbody2D>();
        spriter = GetComponent<SpriteRenderer>();
        wait = new WaitForFixedUpdate();
    }

    void FixedUpdate() {
        if (!isLive || anim.GetCurrentAnimatorStateInfo(0).IsName("Hit"))   //GetCurrentAnimationStateInfo : 현재 상태 정보를 가져오는 함수
            return;

        Vector2 dirVec = target.position - rigid.position;  // 방향 = 위치 차이의 정규화(Normalized). 위치 차이 = 타겟 위치 - 나의 위치.
        Vector2 nextVec = dirVec.normalized * speed * Time.fixedDeltaTime;
        rigid.MovePosition(rigid.position + nextVec);   //플레이어의 키 입력값을 더한 이동 = 몬스터의 방향 값을 더한 이동
        rigid.velocity = Vector2.zero; //물리 속도가 이동에 영향을 주지 않도록 속도 제거
    }

    void LateUpdate() {
        if (!isLive)
            return;
        spriter.flipX = target.position.x < rigid.position.x;
    }

    void OnEnable() {
        target = GameManager.instance.player.GetComponent<Rigidbody2D>();   //플레이어 할당
        isLive = true;
        health = maxHealth;
    }

    //데이터를 가져오기 위한 초기화 함수
    public void Init(SpawnData data) {
        anim.runtimeAnimatorController = animCon[data.spriteType];
        speed = data.speed;
        maxHealth = data.health;
        health = data.health;
    }

    void OnTriggerEnter2D(Collider2D collision) {
        if (!collision.CompareTag("Bullet"))
            return;

        health -= collision.GetComponent<Bullet>().damage;  //총알 데미지만큼 Enemy 체력 감소
        StartCoroutine(KnockBack());

        if(health > 0) {
            anim.SetTrigger("Hit");

        }
        else {
            // .. Die
            Dead();
        }
    }

    IEnumerator KnockBack() {
        yield return wait;  //다음 하나의 물리 프레임 딜레이
        Vector3 playerPos = GameManager.instance.player.transform.position;
        Vector3 dirVec = transform.position - playerPos;
        rigid.AddForce(dirVec.normalized * 3, ForceMode2D.Impulse);
    }

    void Dead() {
        gameObject.SetActive(false);
    }
}
  • 몬스터는 rigidbody를 통해 물리적인 방법으로 플레이어를 향해 이동하기 때문에, Fixed Update에서 animation 상태에 따른 조건을 주지 않으면 KnockBack이 발동하지 않게 된다.
  • 따라서 KnockBack을 정상적으로 실행하려면 위와 같이 Fixed Update의 if문에 조건을 추가해서 Hit 상태일 때는 이동하지 않게끔 만들어줘야한다.

테스트 실행 결과. 적에게 피해를 주니 넉백하면서 Hit 애니메이션이 잘 작동한다.


2. 사망 리액션

//Enemy Script

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Enemy : MonoBehaviour {
    public RuntimeAnimatorController[] animCon;
    public float health;
    public float maxHealth;
    public float speed;
    public Rigidbody2D target;  //쫓아갈 타겟(플레이어)

    bool isLive;

    Rigidbody2D rigid;
    Collider2D coll;
    Animator anim;
    SpriteRenderer spriter;
    WaitForFixedUpdate wait;

    void Awake() {
        rigid = GetComponent<Rigidbody2D>();
        coll = GetComponent<Collider2D>();
        anim = GetComponent<Animator>();
        spriter = GetComponent<SpriteRenderer>();
        wait = new WaitForFixedUpdate();
    }

    void FixedUpdate() {
        if (!isLive || anim.GetCurrentAnimatorStateInfo(0).IsName("Hit"))   //GetCurrentAnimationStateInfo : 현재 상태 정보를 가져오는 함수
            return;

        Vector2 dirVec = target.position - rigid.position;  // 방향 = 위치 차이의 정규화(Normalized). 위치 차이 = 타겟 위치 - 나의 위치.
        Vector2 nextVec = dirVec.normalized * speed * Time.fixedDeltaTime;
        rigid.MovePosition(rigid.position + nextVec);   //플레이어의 키 입력값을 더한 이동 = 몬스터의 방향 값을 더한 이동
        rigid.velocity = Vector2.zero; //물리 속도가 이동에 영향을 주지 않도록 속도 제거
    }

    void LateUpdate() {
        if (!isLive)
            return;
        spriter.flipX = target.position.x < rigid.position.x;
    }

    void OnEnable() {
        target = GameManager.instance.player.GetComponent<Rigidbody2D>();   //플레이어 할당
        isLive = true;
        coll.enabled = true;
        rigid.simulated = true;
        spriter.sortingOrder = 2;
        anim.SetBool("Dead", false);
        health = maxHealth;
    }

    //데이터를 가져오기 위한 초기화 함수
    public void Init(SpawnData data) {
        anim.runtimeAnimatorController = animCon[data.spriteType];
        speed = data.speed;
        maxHealth = data.health;
        health = data.health;
    }

    void OnTriggerEnter2D(Collider2D collision) {
        if (!collision.CompareTag("Bullet"))
            return;

        health -= collision.GetComponent<Bullet>().damage;  //총알 데미지만큼 Enemy 체력 감소
        StartCoroutine(KnockBack());

        if(health > 0) {
            anim.SetTrigger("Hit");

        }
        else {
            isLive = false;
            coll.enabled = false;
            rigid.simulated = false;    //rigidbody의 물리적 비활성화는 simulated를 false로 설정.
            spriter.sortingOrder = 1;   //SpriteRenderer의 Sorting Order를 감소시켜 다른 몬스터를 가리지 않게 함.
            anim.SetBool("Dead", true);
            
        }
    }

    IEnumerator KnockBack() {
        yield return wait;  //다음 하나의 물리 프레임 딜레이
        Vector3 playerPos = GameManager.instance.player.transform.position;
        Vector3 dirVec = transform.position - playerPos;
        rigid.AddForce(dirVec.normalized * 3, ForceMode2D.Impulse);
    }

    void Dead() {
        gameObject.SetActive(false);
    }
}

DeadEnemy 1 Animation을 열고
1초대에 키 프레임을 복사하고, Add Event를 해준다.
앞의 과정을 DeadEnemy 2에서도 똑같이 진행해준다.
이후 Animation 창을 닫지 말고, Enemy 프리펩을 연다. 그런 다음 애니메이션을 DeadEnemy 1을 선택.
Animation에 생성했던 Event를 클릭하면, Inspector창에서 Function 지정 가능
Enemy의 Controller를 AcEnemy2로 변경해 DeadEnemy 2 애니메이션의 Event도 동일하게 함수를 지정해준다

  • 이와 같이 Animation의 Event를 통해서 Enemy 스크립트의 Dead() 함수를 애니메이션을 통해 실행시킬 수 있다.

테스트 실행 결과. 죽는 애니메이션이 실행된 후, 시체가 비활성화되며 사라진다.


3. 처치 데이터 읽기

//GameManager Script

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameManager : MonoBehaviour {
	public static GameManager instance;
	[Header("# Game Control")]
	public float gameTime;	//게임 시간 변수
	public float maxGameTime = 2 * 10f; //최대 게임 시간 변수(20초).
    [Header("# Player Info")]
    public int level;
	public int kill;
	public int exp;
	public int[] nextExp = { 3, 5, 10, 100, 150, 210, 280, 360, 450, 600 };
    [Header("# Game Object")]
    public PoolManager pool;
    public Player player;

    void Awake() {
		instance = this;
	}

	void Update() {
		gameTime += Time.deltaTime;

		if (gameTime > maxGameTime) {
			gameTime = maxGameTime;
		}
	}

	public void GetExp() {
		exp++;

		if (exp == nextExp[level]) {
			level++;
			exp = 0;

		}
	}
}
//Enemy Script

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Enemy : MonoBehaviour {
    public RuntimeAnimatorController[] animCon;
    public float health;
    public float maxHealth;
    public float speed;
    public Rigidbody2D target;  //쫓아갈 타겟(플레이어)

    bool isLive;

    Rigidbody2D rigid;
    Collider2D coll;
    Animator anim;
    SpriteRenderer spriter;
    WaitForFixedUpdate wait;

    void Awake() {
        rigid = GetComponent<Rigidbody2D>();
        coll = GetComponent<Collider2D>();
        anim = GetComponent<Animator>();
        spriter = GetComponent<SpriteRenderer>();
        wait = new WaitForFixedUpdate();
    }

    void FixedUpdate() {
        if (!isLive || anim.GetCurrentAnimatorStateInfo(0).IsName("Hit"))   //GetCurrentAnimationStateInfo : 현재 상태 정보를 가져오는 함수
            return;

        Vector2 dirVec = target.position - rigid.position;  // 방향 = 위치 차이의 정규화(Normalized). 위치 차이 = 타겟 위치 - 나의 위치.
        Vector2 nextVec = dirVec.normalized * speed * Time.fixedDeltaTime;
        rigid.MovePosition(rigid.position + nextVec);   //플레이어의 키 입력값을 더한 이동 = 몬스터의 방향 값을 더한 이동
        rigid.velocity = Vector2.zero; //물리 속도가 이동에 영향을 주지 않도록 속도 제거
    }

    void LateUpdate() {
        if (!isLive)
            return;
        spriter.flipX = target.position.x < rigid.position.x;
    }

    void OnEnable() {
        target = GameManager.instance.player.GetComponent<Rigidbody2D>();   //플레이어 할당
        isLive = true;
        coll.enabled = true;
        rigid.simulated = true;
        spriter.sortingOrder = 2;
        anim.SetBool("Dead", false);
        health = maxHealth;
    }

    //데이터를 가져오기 위한 초기화 함수
    public void Init(SpawnData data) {
        anim.runtimeAnimatorController = animCon[data.spriteType];
        speed = data.speed;
        maxHealth = data.health;
        health = data.health;
    }

    void OnTriggerEnter2D(Collider2D collision) {
        if (!collision.CompareTag("Bullet") || !isLive)
            return;

        health -= collision.GetComponent<Bullet>().damage;  //총알 데미지만큼 Enemy 체력 감소
        StartCoroutine(KnockBack());

        if(health > 0) {
            anim.SetTrigger("Hit");

        }
        else {
            isLive = false;
            coll.enabled = false;
            rigid.simulated = false;    //rigidbody의 물리적 비활성화는 simulated를 false로 설정.
            spriter.sortingOrder = 1;   //SpriteRenderer의 Sorting Order를 감소시켜 다른 몬스터를 가리지 않게 함.
            anim.SetBool("Dead", true);
            GameManager.instance.kill++;
            GameManager.instance.GetExp();
        }
    }

    IEnumerator KnockBack() {
        yield return wait;  //다음 하나의 물리 프레임 딜레이
        Vector3 playerPos = GameManager.instance.player.transform.position;
        Vector3 dirVec = transform.position - playerPos;
        rigid.AddForce(dirVec.normalized * 3, ForceMode2D.Impulse);
    }

    void Dead() {
        gameObject.SetActive(false);
    }
}

테스트 실행 결과. 적을 처치하면 Kill수가 오르고, Exp가 올라간다. 미리 지정했던만큼의 경험치를 채우면, 레벨업도 한다.