기록 보관소

[Unity/유니티] 기초-3D 쿼터뷰 액션 게임: 다양한 몬스터 만들기[B49] 본문

유니티 프로젝트/3D 쿼터뷰 액션게임

[Unity/유니티] 기초-3D 쿼터뷰 액션 게임: 다양한 몬스터 만들기[B49]

JongHoon 2022. 4. 3. 23:55

개요

유니티 입문과 독학을 위해서 아래 링크의 골드메탈님의 영상들을 보며 진행 상황 사진 또는 캡처를 올리고 배웠던 점을 요약해서 적는다.

현재는 영상들을 보고 따라하고 배우는 것에 집중할 것이며, 영상을 모두 보고 따라한 후에는 개인 프로젝트를 설계하고 직접 만드는 것이 목표다.

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

 

유니티 강좌 기초 채널 Basic

유니티 개발을 처음 시작하시는 입문자 분들을 위한 기초 채널. [ 프로젝트 ] B00 ~ B12 (BE1) : 유니티 필수 기초 B13 ~ B19 (BE2) : 2D 플랫포머 B20 ~ B26 (BE3) : 2D 탑다운 대화형 RPG B27 ~ B37 (BE4) : 2D 종스크롤

www.youtube.com


3D 쿼터뷰 액션 게임: 다양한 몬스터 만들기[B49]

1. 플레이어 피격

//Player 스크립트 파일

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

public class Player : MonoBehaviour {
    public float speed;
	public GameObject[] weapons;
	public bool[] hasWeapons;
	public GameObject[] grenades;
	public int hasGrenades;
	public GameObject grenadeObj;
	public Camera followCamera;

	public int ammo;
	public int coin;
	public int health;

	public int maxAmmo;
	public int maxCoin;
	public int maxHealth;
	public int maxHasGrenades;

	float hAxis;
    float vAxis;

    bool wDown;
    bool jDown;
	bool fDown;
	bool gDown;
	bool rDown;
	bool iDown;
	bool sDown1;	//망치
	bool sDown2;	//권총
	bool sDown3;	//기관총

    bool isJump;
	bool isDodge;
	bool isSwap;
	bool isReload;
	bool isFireReady = true;
	bool isBorder;
	bool isDamage;

    Vector3 moveVec;
	Vector3 dodgeVec;

    Animator anim;
    Rigidbody rigid;
	MeshRenderer[] meshs;

	GameObject nearObject;
	Weapon equipWeapon;
	int equipWeaponIndex = -1;
	float fireDelay;

    void Awake() {
        rigid = GetComponent<Rigidbody>();
        anim = GetComponentInChildren<Animator>();  //Player 자식 오브젝트에 있으므로
		meshs = GetComponentsInChildren<MeshRenderer>();
    }

    void Update() {
        GetInput();
        Move();
        Turn();
        Jump();
		Grenade();
		Attack();
		Reload();
		Dodge();
		Swap();
		Interation();
    }

    void GetInput() {
        hAxis = Input.GetAxisRaw("Horizontal");	//좌우 방향키
        vAxis = Input.GetAxisRaw("Vertical");	//상하 방향키
        wDown = Input.GetButton("Walk");	//shift 키
		jDown = Input.GetButtonDown("Jump");	//스페이스바
		fDown = Input.GetButton("Fire1");   //마우스 왼쪽 클릭
		gDown = Input.GetButton("Fire2");   //마우스 오른쪽 클릭
		rDown = Input.GetButtonDown("Reload");	//R키
		iDown = Input.GetButtonDown("Interation");	//E키
		sDown1 = Input.GetButton("Swap1");	//번호 1번 키
		sDown2 = Input.GetButton("Swap2");	//번호 2번 키
		sDown3 = Input.GetButton("Swap3");	//번호 3번 키
	}

    void Move() {
        moveVec = new Vector3(hAxis, 0, vAxis).normalized;  //normalized : 방향 값이 1로 보정된 벡터

		if (isDodge)	//회피 중일때는
			moveVec = dodgeVec; //회피하는 중인 방향으로 유지

		if (isSwap || isReload || !isFireReady)	//무기 교체, 재장전, 공격 중일때는
			moveVec = Vector3.zero;	//멈추기

		if (!isBorder)
			transform.position += moveVec * speed * (wDown ? 0.3f : 1f) * Time.deltaTime;

		anim.SetBool("isRun", (moveVec != Vector3.zero));   //이동을 멈추면
        anim.SetBool("isWalk", wDown);
    }

    void Turn() {
		//키보드로 회전
		transform.LookAt(transform.position + moveVec); //나아갈 방향 보기

		//마우스로 회전
		if (fDown) {
			Ray ray = followCamera.ScreenPointToRay(Input.mousePosition);
			RaycastHit rayHit;
			if (Physics.Raycast(ray, out rayHit, 100)) {
				Vector3 nextVec = rayHit.point - transform.position;
				nextVec.y = 0;
				transform.LookAt(transform.position + nextVec);
			}
		}
    }

    void Jump() {
        if (jDown && (moveVec == Vector3.zero) && !isJump && !isDodge && !isSwap) {	//움직이지 않고 점프
            rigid.AddForce(Vector3.up * 15, ForceMode.Impulse);
			anim.SetBool("isJump", true);
			anim.SetTrigger("doJump");
			isJump = true;
        }
    }

	void Grenade() {
		if (hasGrenades == 0)
			return;

		if (gDown && !isReload && !isSwap) {
			Ray ray = followCamera.ScreenPointToRay(Input.mousePosition);
			RaycastHit rayHit;
			if (Physics.Raycast(ray, out rayHit, 100)) {
				Vector3 nextVec = rayHit.point - transform.position;
				nextVec.y = 10;

				GameObject instantGrenade = Instantiate(grenadeObj, transform.position, transform.rotation);
				Rigidbody rigidGrenade = instantGrenade.GetComponent<Rigidbody>();
				rigidGrenade.AddForce(nextVec, ForceMode.Impulse);
				rigidGrenade.AddTorque(Vector3.back * 10, ForceMode.Impulse);

				hasGrenades--;
				grenades[hasGrenades].SetActive(false);
			}
		}
	}

	void Attack() {
		if (equipWeapon == null)
			return;

		fireDelay += Time.deltaTime;
		isFireReady = (equipWeapon.rate < fireDelay);

		if (fDown && isFireReady && !isDodge && !isSwap && !isReload) {
			equipWeapon.Use();
			anim.SetTrigger(equipWeapon.type == Weapon.Type.Melee ? "doSwing" : "doShot");
			fireDelay = 0;
		}
	}

	void Reload() {
		if (equipWeapon == null)
			return;

		if (equipWeapon.type == Weapon.Type.Melee)
			return;

		if (ammo == 0)
			return;

		if (rDown && !isJump && !isDodge && !isSwap && isFireReady) {
			anim.SetTrigger("doReload");
			isReload = true;

			Invoke("ReloadOut", 3);
		}
	}

	void ReloadOut() { 
		int reAmmo = (ammo < equipWeapon.maxAmmo) ? ammo : equipWeapon.maxAmmo;
		equipWeapon.curAmmo = reAmmo;
		ammo -= reAmmo;
		isReload = false;
	}

	void Dodge() {
		if (jDown && (moveVec != Vector3.zero) && !isJump && !isDodge && !isSwap) {    //이동하면서 점프
			dodgeVec = moveVec;
			speed *= 2;
			anim.SetTrigger("doDodge");
			isDodge = true;

			Invoke("DodgeOut", 0.4f);
		}
	}

	void DodgeOut() {
		speed *= 0.5f;
		isDodge = false;
	}

	void Swap() {
		if (sDown1 && (!hasWeapons[0] || equipWeaponIndex == 0))	return;
		if (sDown2 && (!hasWeapons[1] || equipWeaponIndex == 1))	return;
		if (sDown3 && (!hasWeapons[2] || equipWeaponIndex == 2))	return;

		int weaponIndex = -1;
		if (sDown1) weaponIndex = 0;
		if (sDown2) weaponIndex = 1;
		if (sDown3) weaponIndex = 2;

		if ((sDown1 || sDown2 || sDown3) && !isJump && !isDodge) {
			if (equipWeapon != null)
				equipWeapon.gameObject.SetActive(false);

			equipWeaponIndex = weaponIndex;
			equipWeapon = weapons[weaponIndex].GetComponent<Weapon>();
			equipWeapon.gameObject.SetActive(true);

			anim.SetTrigger("doSwap");
			isSwap = true;
			Invoke("SwapOut", 0.4f);
		}
	}

	void SwapOut() {
		isSwap = false;
	}

	void Interation() {
		if (iDown && nearObject != null && !isJump && !isDodge) {
			if (nearObject.tag == "Weapon") {
				Item item = nearObject.GetComponent<Item>();
				int weaponIndex = item.value;
				hasWeapons[weaponIndex] = true;

				Destroy(nearObject);
			}
		}
	}

	void FreezeRotation() {
		rigid.angularVelocity = Vector3.zero;
	}

	void StopToWall() {
		Debug.DrawRay(transform.position, transform.forward * 5, Color.green);
		isBorder = Physics.Raycast(transform.position, moveVec, 5, LayerMask.GetMask("Wall"));
	}

	void FixedUpdate() {
		FreezeRotation();
		StopToWall();
	}

	void OnCollisionEnter(Collision collision) {
		if (collision.gameObject.tag == "Floor") {
			anim.SetBool("isJump", false);
			isJump = false;
		}
	}

	void OnTriggerEnter(Collider other) {
		if (other.tag == "Item") {
			Item item = other.GetComponent<Item>();
			switch(item.type) {
				case Item.Type.Ammo:
					ammo += item.value;
					if (ammo > maxAmmo)
						ammo = maxAmmo;
					break;
				case Item.Type.Coin:
					coin += item.value;
					if (coin > maxCoin)
						coin = maxCoin;
					break;
				case Item.Type.Heart:
					health += item.value;
					if (health > maxHealth)
						health = maxHealth;
					break;
				case Item.Type.Grenade:
					if (hasGrenades == maxHasGrenades)
						return;

					grenades[hasGrenades].SetActive(true);
					hasGrenades += item.value;
					if (hasGrenades > maxHasGrenades)
						hasGrenades = maxHasGrenades;
					break;
			}
			Destroy(other.gameObject);
		}
		else if (other.tag == "EnemyBullet") {
			if (!isDamage) {
				Bullet enemyBullet = other.GetComponent<Bullet>();
				health -= enemyBullet.damage;

				StartCoroutine(OnDamage());
			}
		}
	}

	IEnumerator OnDamage() {
		isDamage = true;
		foreach(MeshRenderer mesh in meshs) {
			mesh.material.color = Color.yellow;
		}

		yield return new WaitForSeconds(1f);

		isDamage = false;

		foreach (MeshRenderer mesh in meshs) {
			mesh.material.color = Color.white;
		}
	}

	void OnTriggerStay(Collider other) {
		if (other.tag == "Weapon")
			nearObject = other.gameObject;
	}

	void OnTriggerExit(Collider other) {
		if (other.tag == "Weapon")
			nearObject = null;
	}
}

테스트를 위한 Collider와 Bullet 스크립트가 있는 Empty 생성. Tag와 Layer도 설정해준다
플레이어가 이 범위에 닿으면 색이 노랗게 변하고 체력이 감소할 것이다
초기 플레이어 체력 100
1초동안 노란색으로 변하면서 체력이 90이 되었다
1초 후 다시 닿으니 체력이 10닳고 노란색으로 변한 모습


2. 몬스터 움직임 보완

//Enemy 스크립트 파일

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

public class Enemy : MonoBehaviour {
	public int maxHealth;
	public int curHealth;
	public Transform target;
	public bool isChase;

	Rigidbody rigid;
	BoxCollider boxCollider;
	Material mat;
	NavMeshAgent nav;
	Animator anim;

	void Awake() {
		rigid = GetComponent<Rigidbody>();
		boxCollider = GetComponent<BoxCollider>();
		mat = GetComponentInChildren<MeshRenderer>().material;
		nav = GetComponent<NavMeshAgent>();
		anim = GetComponentInChildren<Animator>();

		Invoke("ChaseStart", 2);
	}

	void ChaseStart() {
		isChase = true;
		anim.SetBool("isWalk", true);
	}

	void Update() {
		if (nav.enabled) {
			nav.SetDestination(target.position);
			nav.isStopped = !isChase;
		}
	}

	void FreezeVelocity() {
		if (isChase) {
			rigid.velocity = Vector3.zero;
			rigid.angularVelocity = Vector3.zero;
		}
	}

	void FixedUpdate() {
		FreezeVelocity();
	}

	void OnTriggerEnter(Collider other) {
		if (other.tag == "Melee") {
			Weapon weapon = other.GetComponent<Weapon>();
			curHealth -= weapon.damage;
			Vector3 reactVec = transform.position - other.transform.position;

			StartCoroutine(OnDamage(reactVec, false));
		}
		else if (other.tag == "Bullet") {
			Bullet bullet = other.GetComponent<Bullet>();
			curHealth -= bullet.damage;
			Vector3 reactVec = transform.position - other.transform.position;
			Destroy(other.gameObject);

			StartCoroutine(OnDamage(reactVec, false));
		}
	}

	public void HitByGrenade(Vector3 explosionPos) {
		curHealth -= 100;
		Vector3 reactVec = transform.position - explosionPos;

		StartCoroutine(OnDamage(reactVec, true));
	}

	IEnumerator OnDamage(Vector3 reactVec, bool isGrenade) {
		mat.color = Color.red;
		yield return new WaitForSeconds(0.1f);

		if (curHealth > 0) {
			mat.color = Color.white;
		}
		else {
			mat.color = Color.gray;
			gameObject.layer = 12;  //Enemy Dead 레이어로 변경
			isChase = false;
			nav.enabled = false;
			anim.SetTrigger("doDie");

			if (isGrenade) {
				reactVec = reactVec.normalized;
				reactVec += Vector3.up * 5;
				rigid.freezeRotation = false;
				rigid.AddForce(reactVec * 5, ForceMode.Impulse);
				rigid.AddTorque(reactVec * 15, ForceMode.Impulse);
			}
			else {
				reactVec = reactVec.normalized;
				reactVec += Vector3.up;
				rigid.AddForce(reactVec * 5, ForceMode.Impulse);
			}

			Destroy(gameObject, 4);
		}
	}
}
  • 기존의 isChase를 통한 로직은 목표만 잃어버리는 것이라 이동이 유지된다. 그래서 nav가 활성화 되었을 때로 if문을 변경하고, 내부에 nav의 isStopped라는 함수 조건으로 isChase가 false일때를 넣어 멈추도록 해준다.

3. 일반형 몬스터

Enemy A의 근접 공격 범위로 사용하기위해서 Enemy Bullet을 자식 오브젝트로 넣어준다

//Enemy 스크립트 파일

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

public class Enemy : MonoBehaviour {
	public int maxHealth;
	public int curHealth;
	public Transform target;
	public BoxCollider meleeArea;
	public bool isChase;
	public bool isAttack;

	Rigidbody rigid;
	BoxCollider boxCollider;
	Material mat;
	NavMeshAgent nav;
	Animator anim;

	void Awake() {
		rigid = GetComponent<Rigidbody>();
		boxCollider = GetComponent<BoxCollider>();
		mat = GetComponentInChildren<MeshRenderer>().material;
		nav = GetComponent<NavMeshAgent>();
		anim = GetComponentInChildren<Animator>();

		Invoke("ChaseStart", 2);
	}

	void ChaseStart() {
		isChase = true;
		anim.SetBool("isWalk", true);
	}

	void Update() {
		if (nav.enabled) {
			nav.SetDestination(target.position);
			nav.isStopped = !isChase;
		}
	}

	void FreezeVelocity() {
		if (isChase) {
			rigid.velocity = Vector3.zero;
			rigid.angularVelocity = Vector3.zero;
		}
	}

	void Targeting() {
		float targetRadius = 1.5f;
		float targetRange = 3f;

		RaycastHit[] rayHits = Physics.SphereCastAll(transform.position, targetRadius, transform.forward, targetRange, LayerMask.GetMask("Player"));

		if (rayHits.Length > 0 && !isAttack) {
			StartCoroutine(Attack());
		}
	}

	IEnumerator Attack() {
		isChase = false;
		isAttack = true;
		anim.SetBool("isAttack", true);

		yield return new WaitForSeconds(0.2f);
		meleeArea.enabled = true;

		yield return new WaitForSeconds(1f);
		meleeArea.enabled = false;

		yield return new WaitForSeconds(1f);

		isChase = true;
		isAttack = false;
		anim.SetBool("isAttack", false);
	}

	void FixedUpdate() {
		Targeting();
		FreezeVelocity();
	}

	void OnTriggerEnter(Collider other) {
		if (other.tag == "Melee") {
			Weapon weapon = other.GetComponent<Weapon>();
			curHealth -= weapon.damage;
			Vector3 reactVec = transform.position - other.transform.position;

			StartCoroutine(OnDamage(reactVec, false));
		}
		else if (other.tag == "Bullet") {
			Bullet bullet = other.GetComponent<Bullet>();
			curHealth -= bullet.damage;
			Vector3 reactVec = transform.position - other.transform.position;
			Destroy(other.gameObject);

			StartCoroutine(OnDamage(reactVec, false));
		}
	}

	public void HitByGrenade(Vector3 explosionPos) {
		curHealth -= 100;
		Vector3 reactVec = transform.position - explosionPos;

		StartCoroutine(OnDamage(reactVec, true));
	}

	IEnumerator OnDamage(Vector3 reactVec, bool isGrenade) {
		mat.color = Color.red;
		yield return new WaitForSeconds(0.1f);

		if (curHealth > 0) {
			mat.color = Color.white;
		}
		else {
			mat.color = Color.gray;
			gameObject.layer = 12;  //Enemy Dead 레이어로 변경
			isChase = false;
			nav.enabled = false;
			anim.SetTrigger("doDie");

			if (isGrenade) {
				reactVec = reactVec.normalized;
				reactVec += Vector3.up * 5;
				rigid.freezeRotation = false;
				rigid.AddForce(reactVec * 5, ForceMode.Impulse);
				rigid.AddTorque(reactVec * 15, ForceMode.Impulse);
			}
			else {
				reactVec = reactVec.normalized;
				reactVec += Vector3.up;
				rigid.AddForce(reactVec * 5, ForceMode.Impulse);
			}

			Destroy(gameObject, 4);
		}
	}
}

근접 공격 범위에 EnemyBullet을 넣어준다
몬스터의 공격 애니메이션과 공격 당하는 플레이어


4. 돌격형 몬스터

돌격형 몬스터로 사용할 Enemy B 생성
Rigidbody, Box Collider, Nav Mesh Agent, Enemy 스크립트 파일 등 설정
Enemy A 애니메이터를 복사해서 Enemy B로 이름을 바꾸고 Mesh Object에 넣어주기
Enemy A의 Nav Mesh Agent 설정 변경
Enemy b는 돌격형이니 조금 더 빠르게한다

  • Angular Speed : Nav Agent의 회전 속도
  • Aceleration : Nav Agnet의 가속도
//Enemy 스크립트 파일

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

public class Enemy : MonoBehaviour {
	public enum Type { A, B, C };
	public Type enemyType;
	public int maxHealth;
	public int curHealth;
	public Transform target;
	public BoxCollider meleeArea;
	public bool isChase;
	public bool isAttack;

	Rigidbody rigid;
	BoxCollider boxCollider;
	Material mat;
	NavMeshAgent nav;
	Animator anim;

	void Awake() {
		rigid = GetComponent<Rigidbody>();
		boxCollider = GetComponent<BoxCollider>();
		mat = GetComponentInChildren<MeshRenderer>().material;
		nav = GetComponent<NavMeshAgent>();
		anim = GetComponentInChildren<Animator>();

		Invoke("ChaseStart", 2);
	}

	void ChaseStart() {
		isChase = true;
		anim.SetBool("isWalk", true);
	}

	void Update() {
		if (nav.enabled) {
			nav.SetDestination(target.position);
			nav.isStopped = !isChase;
		}
	}

	void FreezeVelocity() {
		if (isChase) {
			rigid.velocity = Vector3.zero;
			rigid.angularVelocity = Vector3.zero;
		}
	}

	void Targeting() {
		float targetRadius = 0;
		float targetRange = 0;

		switch(enemyType) {
			case Type.A:
				targetRadius = 1.5f;
				targetRange = 3f;
				break;
			case Type.B:
				targetRadius = 1f;
				targetRange = 12f;
				break;
			case Type.C:

				break;
		}

		RaycastHit[] rayHits = Physics.SphereCastAll(transform.position, targetRadius, transform.forward, targetRange, LayerMask.GetMask("Player"));

		if (rayHits.Length > 0 && !isAttack) {
			StartCoroutine(Attack());
		}
	}

	IEnumerator Attack() {
		isChase = false;
		isAttack = true;
		anim.SetBool("isAttack", true);

		switch(enemyType) {
			case Type.A:
				yield return new WaitForSeconds(0.2f);
				meleeArea.enabled = true;

				yield return new WaitForSeconds(1f);
				meleeArea.enabled = false;

				yield return new WaitForSeconds(1f);
				break;
			case Type.B:
				yield return new WaitForSeconds(0.1f);
				rigid.AddForce(transform.forward * 20, ForceMode.Impulse);
				meleeArea.enabled = true;

				yield return new WaitForSeconds(0.5f);
				rigid.velocity = Vector3.zero;
				meleeArea.enabled = false;

				yield return new WaitForSeconds(2f);
				break;
			case Type.C:

				break;
		}

		isChase = true;
		isAttack = false;
		anim.SetBool("isAttack", false);
	}

	void FixedUpdate() {
		Targeting();
		FreezeVelocity();
	}

	void OnTriggerEnter(Collider other) {
		if (other.tag == "Melee") {
			Weapon weapon = other.GetComponent<Weapon>();
			curHealth -= weapon.damage;
			Vector3 reactVec = transform.position - other.transform.position;

			StartCoroutine(OnDamage(reactVec, false));
		}
		else if (other.tag == "Bullet") {
			Bullet bullet = other.GetComponent<Bullet>();
			curHealth -= bullet.damage;
			Vector3 reactVec = transform.position - other.transform.position;
			Destroy(other.gameObject);

			StartCoroutine(OnDamage(reactVec, false));
		}
	}

	public void HitByGrenade(Vector3 explosionPos) {
		curHealth -= 100;
		Vector3 reactVec = transform.position - explosionPos;

		StartCoroutine(OnDamage(reactVec, true));
	}

	IEnumerator OnDamage(Vector3 reactVec, bool isGrenade) {
		mat.color = Color.red;
		yield return new WaitForSeconds(0.1f);

		if (curHealth > 0) {
			mat.color = Color.white;
		}
		else {
			mat.color = Color.gray;
			gameObject.layer = 12;  //Enemy Dead 레이어로 변경
			isChase = false;
			nav.enabled = false;
			anim.SetTrigger("doDie");

			if (isGrenade) {
				reactVec = reactVec.normalized;
				reactVec += Vector3.up * 5;
				rigid.freezeRotation = false;
				rigid.AddForce(reactVec * 5, ForceMode.Impulse);
				rigid.AddTorque(reactVec * 15, ForceMode.Impulse);
			}
			else {
				reactVec = reactVec.normalized;
				reactVec += Vector3.up;
				rigid.AddForce(reactVec * 5, ForceMode.Impulse);
			}

			Destroy(gameObject, 4);
		}
	}
}

Enemy A 설정
Enemy B 설정
Enemy B가 훨씬 빠르게 쫓아온다
Enemy B가 돌격 후 공격하는 모습


5. 원거리형 몬스터

Enemy C 추가 및 설정
몬스터가 사용할 미사일 추가 및 설정
Missile의 Mesh Object에 Missile 스크립트 파일 생성 및 추가

//Missile 스크립트 파일

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

public class Missile : MonoBehaviour {
    void Update() {
		transform.Rotate(Vector3.right * 30 * Time.deltaTime);
    }
}

파티클 이펙트를 위한 Effect 생성 및 추가
Rigidbody와 Box Collider, Bullet 스크립트 파일을 추가하고, Tag와 Layer도 변경해준다
완성된 미사일은 프리펩으로 만들어주고, Z축이 나아가는 방향이 되도록 Effect와 Mesh Object를 일부 수정한다
중력도 꺼준다
원거리 공격 애니메이션 추가를 위해 Enemy B 애니메이터를 복사해 Enemy C로 만들어준다
Enemy C는 모델이 다르므로 모션을 모두 Enemy C의 애니메이션 모션으로 변경해준다
Enemy A B C 모두 Tag와 Layer를 Enemy로 변경
Enemy와 EnemyBullet이 충돌하지 않도록 변경

//Enemy 스크립트 파일

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

public class Enemy : MonoBehaviour {
	public enum Type { A, B, C };
	public Type enemyType;
	public int maxHealth;
	public int curHealth;
	public Transform target;
	public BoxCollider meleeArea;
	public GameObject bullet;
	public bool isChase;
	public bool isAttack;

	Rigidbody rigid;
	BoxCollider boxCollider;
	Material mat;
	NavMeshAgent nav;
	Animator anim;

	void Awake() {
		rigid = GetComponent<Rigidbody>();
		boxCollider = GetComponent<BoxCollider>();
		mat = GetComponentInChildren<MeshRenderer>().material;
		nav = GetComponent<NavMeshAgent>();
		anim = GetComponentInChildren<Animator>();

		Invoke("ChaseStart", 2);
	}

	void ChaseStart() {
		isChase = true;
		anim.SetBool("isWalk", true);
	}

	void Update() {
		if (nav.enabled) {
			nav.SetDestination(target.position);
			nav.isStopped = !isChase;
		}
	}

	void FreezeVelocity() {
		if (isChase) {
			rigid.velocity = Vector3.zero;
			rigid.angularVelocity = Vector3.zero;
		}
	}

	void Targeting() {
		float targetRadius = 0;
		float targetRange = 0;

		switch(enemyType) {
			case Type.A:
				targetRadius = 1.5f;
				targetRange = 3f;
				break;
			case Type.B:
				targetRadius = 1f;
				targetRange = 12f;
				break;
			case Type.C:
				targetRadius = 0.5f;
				targetRange = 25f;
				break;
		}

		RaycastHit[] rayHits = Physics.SphereCastAll(transform.position, targetRadius, transform.forward, targetRange, LayerMask.GetMask("Player"));

		if (rayHits.Length > 0 && !isAttack) {
			StartCoroutine(Attack());
		}
	}

	IEnumerator Attack() {
		isChase = false;
		isAttack = true;
		anim.SetBool("isAttack", true);

		switch(enemyType) {
			case Type.A:
				yield return new WaitForSeconds(0.2f);
				meleeArea.enabled = true;

				yield return new WaitForSeconds(1f);
				meleeArea.enabled = false;

				yield return new WaitForSeconds(1f);
				break;
			case Type.B:
				yield return new WaitForSeconds(0.1f);
				rigid.AddForce(transform.forward * 20, ForceMode.Impulse);
				meleeArea.enabled = true;

				yield return new WaitForSeconds(0.5f);
				rigid.velocity = Vector3.zero;
				meleeArea.enabled = false;

				yield return new WaitForSeconds(2f);
				break;
			case Type.C:
				yield return new WaitForSeconds(0.5f);
				GameObject instantBullet = Instantiate(bullet, transform.position, transform.rotation);
				Rigidbody rigidBullet = instantBullet.GetComponent<Rigidbody>();
				rigidBullet.velocity = transform.forward * 20;

				yield return new WaitForSeconds(2f);
				break;
		}

		isChase = true;
		isAttack = false;
		anim.SetBool("isAttack", false);
	}

	void FixedUpdate() {
		Targeting();
		FreezeVelocity();
	}

	void OnTriggerEnter(Collider other) {
		if (other.tag == "Melee") {
			Weapon weapon = other.GetComponent<Weapon>();
			curHealth -= weapon.damage;
			Vector3 reactVec = transform.position - other.transform.position;

			StartCoroutine(OnDamage(reactVec, false));
		}
		else if (other.tag == "Bullet") {
			Bullet bullet = other.GetComponent<Bullet>();
			curHealth -= bullet.damage;
			Vector3 reactVec = transform.position - other.transform.position;
			Destroy(other.gameObject);

			StartCoroutine(OnDamage(reactVec, false));
		}
	}

	public void HitByGrenade(Vector3 explosionPos) {
		curHealth -= 100;
		Vector3 reactVec = transform.position - explosionPos;

		StartCoroutine(OnDamage(reactVec, true));
	}

	IEnumerator OnDamage(Vector3 reactVec, bool isGrenade) {
		mat.color = Color.red;
		yield return new WaitForSeconds(0.1f);

		if (curHealth > 0) {
			mat.color = Color.white;
		}
		else {
			mat.color = Color.gray;
			gameObject.layer = 12;  //Enemy Dead 레이어로 변경
			isChase = false;
			nav.enabled = false;
			anim.SetTrigger("doDie");

			if (isGrenade) {
				reactVec = reactVec.normalized;
				reactVec += Vector3.up * 5;
				rigid.freezeRotation = false;
				rigid.AddForce(reactVec * 5, ForceMode.Impulse);
				rigid.AddTorque(reactVec * 15, ForceMode.Impulse);
			}
			else {
				reactVec = reactVec.normalized;
				reactVec += Vector3.up;
				rigid.AddForce(reactVec * 5, ForceMode.Impulse);
			}

			Destroy(gameObject, 4);
		}
	}
}
//Player 스크립트 파일

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

public class Player : MonoBehaviour {
    public float speed;
	public GameObject[] weapons;
	public bool[] hasWeapons;
	public GameObject[] grenades;
	public int hasGrenades;
	public GameObject grenadeObj;
	public Camera followCamera;

	public int ammo;
	public int coin;
	public int health;

	public int maxAmmo;
	public int maxCoin;
	public int maxHealth;
	public int maxHasGrenades;

	float hAxis;
    float vAxis;

    bool wDown;
    bool jDown;
	bool fDown;
	bool gDown;
	bool rDown;
	bool iDown;
	bool sDown1;	//망치
	bool sDown2;	//권총
	bool sDown3;	//기관총

    bool isJump;
	bool isDodge;
	bool isSwap;
	bool isReload;
	bool isFireReady = true;
	bool isBorder;
	bool isDamage;

    Vector3 moveVec;
	Vector3 dodgeVec;

    Animator anim;
    Rigidbody rigid;
	MeshRenderer[] meshs;

	GameObject nearObject;
	Weapon equipWeapon;
	int equipWeaponIndex = -1;
	float fireDelay;

    void Awake() {
        rigid = GetComponent<Rigidbody>();
        anim = GetComponentInChildren<Animator>();  //Player 자식 오브젝트에 있으므로
		meshs = GetComponentsInChildren<MeshRenderer>();
    }

    void Update() {
        GetInput();
        Move();
        Turn();
        Jump();
		Grenade();
		Attack();
		Reload();
		Dodge();
		Swap();
		Interation();
    }

    void GetInput() {
        hAxis = Input.GetAxisRaw("Horizontal");	//좌우 방향키
        vAxis = Input.GetAxisRaw("Vertical");	//상하 방향키
        wDown = Input.GetButton("Walk");	//shift 키
		jDown = Input.GetButtonDown("Jump");	//스페이스바
		fDown = Input.GetButton("Fire1");   //마우스 왼쪽 클릭
		gDown = Input.GetButton("Fire2");   //마우스 오른쪽 클릭
		rDown = Input.GetButtonDown("Reload");	//R키
		iDown = Input.GetButtonDown("Interation");	//E키
		sDown1 = Input.GetButton("Swap1");	//번호 1번 키
		sDown2 = Input.GetButton("Swap2");	//번호 2번 키
		sDown3 = Input.GetButton("Swap3");	//번호 3번 키
	}

    void Move() {
        moveVec = new Vector3(hAxis, 0, vAxis).normalized;  //normalized : 방향 값이 1로 보정된 벡터

		if (isDodge)	//회피 중일때는
			moveVec = dodgeVec; //회피하는 중인 방향으로 유지

		if (isSwap || isReload || !isFireReady)	//무기 교체, 재장전, 공격 중일때는
			moveVec = Vector3.zero;	//멈추기

		if (!isBorder)
			transform.position += moveVec * speed * (wDown ? 0.3f : 1f) * Time.deltaTime;

		anim.SetBool("isRun", (moveVec != Vector3.zero));   //이동을 멈추면
        anim.SetBool("isWalk", wDown);
    }

    void Turn() {
		//키보드로 회전
		transform.LookAt(transform.position + moveVec); //나아갈 방향 보기

		//마우스로 회전
		if (fDown) {
			Ray ray = followCamera.ScreenPointToRay(Input.mousePosition);
			RaycastHit rayHit;
			if (Physics.Raycast(ray, out rayHit, 100)) {
				Vector3 nextVec = rayHit.point - transform.position;
				nextVec.y = 0;
				transform.LookAt(transform.position + nextVec);
			}
		}
    }

    void Jump() {
        if (jDown && (moveVec == Vector3.zero) && !isJump && !isDodge && !isSwap) {	//움직이지 않고 점프
            rigid.AddForce(Vector3.up * 15, ForceMode.Impulse);
			anim.SetBool("isJump", true);
			anim.SetTrigger("doJump");
			isJump = true;
        }
    }

	void Grenade() {
		if (hasGrenades == 0)
			return;

		if (gDown && !isReload && !isSwap) {
			Ray ray = followCamera.ScreenPointToRay(Input.mousePosition);
			RaycastHit rayHit;
			if (Physics.Raycast(ray, out rayHit, 100)) {
				Vector3 nextVec = rayHit.point - transform.position;
				nextVec.y = 10;

				GameObject instantGrenade = Instantiate(grenadeObj, transform.position, transform.rotation);
				Rigidbody rigidGrenade = instantGrenade.GetComponent<Rigidbody>();
				rigidGrenade.AddForce(nextVec, ForceMode.Impulse);
				rigidGrenade.AddTorque(Vector3.back * 10, ForceMode.Impulse);

				hasGrenades--;
				grenades[hasGrenades].SetActive(false);
			}
		}
	}

	void Attack() {
		if (equipWeapon == null)
			return;

		fireDelay += Time.deltaTime;
		isFireReady = (equipWeapon.rate < fireDelay);

		if (fDown && isFireReady && !isDodge && !isSwap && !isReload) {
			equipWeapon.Use();
			anim.SetTrigger(equipWeapon.type == Weapon.Type.Melee ? "doSwing" : "doShot");
			fireDelay = 0;
		}
	}

	void Reload() {
		if (equipWeapon == null)
			return;

		if (equipWeapon.type == Weapon.Type.Melee)
			return;

		if (ammo == 0)
			return;

		if (rDown && !isJump && !isDodge && !isSwap && isFireReady) {
			anim.SetTrigger("doReload");
			isReload = true;

			Invoke("ReloadOut", 3);
		}
	}

	void ReloadOut() { 
		int reAmmo = (ammo < equipWeapon.maxAmmo) ? ammo : equipWeapon.maxAmmo;
		equipWeapon.curAmmo = reAmmo;
		ammo -= reAmmo;
		isReload = false;
	}

	void Dodge() {
		if (jDown && (moveVec != Vector3.zero) && !isJump && !isDodge && !isSwap) {    //이동하면서 점프
			dodgeVec = moveVec;
			speed *= 2;
			anim.SetTrigger("doDodge");
			isDodge = true;

			Invoke("DodgeOut", 0.4f);
		}
	}

	void DodgeOut() {
		speed *= 0.5f;
		isDodge = false;
	}

	void Swap() {
		if (sDown1 && (!hasWeapons[0] || equipWeaponIndex == 0))	return;
		if (sDown2 && (!hasWeapons[1] || equipWeaponIndex == 1))	return;
		if (sDown3 && (!hasWeapons[2] || equipWeaponIndex == 2))	return;

		int weaponIndex = -1;
		if (sDown1) weaponIndex = 0;
		if (sDown2) weaponIndex = 1;
		if (sDown3) weaponIndex = 2;

		if ((sDown1 || sDown2 || sDown3) && !isJump && !isDodge) {
			if (equipWeapon != null)
				equipWeapon.gameObject.SetActive(false);

			equipWeaponIndex = weaponIndex;
			equipWeapon = weapons[weaponIndex].GetComponent<Weapon>();
			equipWeapon.gameObject.SetActive(true);

			anim.SetTrigger("doSwap");
			isSwap = true;
			Invoke("SwapOut", 0.4f);
		}
	}

	void SwapOut() {
		isSwap = false;
	}

	void Interation() {
		if (iDown && nearObject != null && !isJump && !isDodge) {
			if (nearObject.tag == "Weapon") {
				Item item = nearObject.GetComponent<Item>();
				int weaponIndex = item.value;
				hasWeapons[weaponIndex] = true;

				Destroy(nearObject);
			}
		}
	}

	void FreezeRotation() {
		rigid.angularVelocity = Vector3.zero;
	}

	void StopToWall() {
		Debug.DrawRay(transform.position, transform.forward * 5, Color.green);
		isBorder = Physics.Raycast(transform.position, moveVec, 5, LayerMask.GetMask("Wall"));
	}

	void FixedUpdate() {
		FreezeRotation();
		StopToWall();
	}

	void OnCollisionEnter(Collision collision) {
		if (collision.gameObject.tag == "Floor") {
			anim.SetBool("isJump", false);
			isJump = false;
		}
	}

	void OnTriggerEnter(Collider other) {
		if (other.tag == "Item") {
			Item item = other.GetComponent<Item>();
			switch(item.type) {
				case Item.Type.Ammo:
					ammo += item.value;
					if (ammo > maxAmmo)
						ammo = maxAmmo;
					break;
				case Item.Type.Coin:
					coin += item.value;
					if (coin > maxCoin)
						coin = maxCoin;
					break;
				case Item.Type.Heart:
					health += item.value;
					if (health > maxHealth)
						health = maxHealth;
					break;
				case Item.Type.Grenade:
					if (hasGrenades == maxHasGrenades)
						return;

					grenades[hasGrenades].SetActive(true);
					hasGrenades += item.value;
					if (hasGrenades > maxHasGrenades)
						hasGrenades = maxHasGrenades;
					break;
			}
			Destroy(other.gameObject);
		}
		else if (other.tag == "EnemyBullet") {
			if (!isDamage) {
				Bullet enemyBullet = other.GetComponent<Bullet>();
				health -= enemyBullet.damage;

				if (other.GetComponent<Rigidbody>() != null)	//Rigidbody 유무를 판단(미사일)
					Destroy(other.gameObject);

				StartCoroutine(OnDamage());
			}
		}
	}

	IEnumerator OnDamage() {
		isDamage = true;
		foreach(MeshRenderer mesh in meshs) {
			mesh.material.color = Color.yellow;
		}

		yield return new WaitForSeconds(1f);

		isDamage = false;

		foreach (MeshRenderer mesh in meshs) {
			mesh.material.color = Color.white;
		}
	}

	void OnTriggerStay(Collider other) {
		if (other.tag == "Weapon")
			nearObject = other.gameObject;
	}

	void OnTriggerExit(Collider other) {
		if (other.tag == "Weapon")
			nearObject = null;
	}
}
//Bullet 스크립트 파일

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

public class Bullet : MonoBehaviour {
	public int damage;
	public bool isMelee;

	void OnCollisionEnter(Collision collision) {
		if (collision.gameObject.tag == "Floor")    //탄피
			Destroy(gameObject, 3); //3초 뒤에 사라지기
	}

	void OnTriggerEnter(Collider other) {	//총알을 isTrigger로 바꾸었으므로
		if(!isMelee && other.gameObject.tag == "Wall")   //총알
			Destroy(gameObject);
	}
}
  • Enemy 스크립트 파일은 Enemy C의 공격을, Player 스크립트 파일은 Enemy C가 발사한 미사일에 접촉시 파괴하는 것을, Bullet 스크립트 파일은 Enemy A와 B의 근접 공격 Box Collider가 Trigger이므로 벽에 닿았을때 사라지는 것을 방지하기위해 수정 및 추가하였다.

Enemy A와 B의 근접 공격 범위에 isMelee 체크
Enemy C의 Bullet에 Missile 추가
미사일을 발사하는 Enemy C
맞으니 피해를 받았다