🎮 Game Dev (게임개발)/PC (데스크탑, 노트북, 터치패널)
[3D 액션게임] 13. 보스 만들기
- -
🔔 유튜브 크리에이터 골든메탈님의 유니티강의 3D 쿼터뷰 액션게임 [BE5] 를 보고 공부하여 작성한 게시글입니다! 🔔
플레이어 스크립트 전체보기
더보기
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 health;
public int coin;
public int maxAmmo;
public int maxHealth;
public int maxCoin;
public int maxHasGrenades;
float hAxis;
float vAxis;
bool rDown;
bool jDown;
bool fDown;
bool gDown;
bool reDown;
bool iDown;
bool sDown1;
bool sDown2;
bool sDown3;
bool isJump;
bool isDodge;
bool isSwap;
bool isReload = false;
bool isFireReady = true;
bool isBorder;
bool isDamage;
Vector3 moveVec;
Vector3 dodgeVec;
Rigidbody rigid;
Animator anim;
MeshRenderer[] meshs;
GameObject nearObject;
Weapon equipWeapon;
int equipWeaponIndex = -1;
float fireDelay;
void Awake()
{
rigid = GetComponent<Rigidbody>();
anim = GetComponentInChildren<Animator>();
meshs = GetComponentsInChildren<MeshRenderer>();
}
// Update is called once per frame
void Update()
{
GetInput();
Move();
Turn();
Jump();
Grenade();
Attack();
Reload();
Dodge();
Swap();
Interation();
}
void GetInput()
{
// 키보드입력에 따라 0 ~ 1로 변환 left right up down
hAxis = Input.GetAxisRaw("Horizontal");
vAxis = Input.GetAxisRaw("Vertical");
rDown = Input.GetButton("Run");
jDown = Input.GetButtonDown("Jump");
reDown = Input.GetButtonDown("Reload");
fDown = Input.GetButton("Fire1");
gDown = Input.GetButtonDown("Fire2");
iDown = Input.GetButtonDown("Interation");
sDown1 = Input.GetButtonDown("Swap1");
sDown2 = Input.GetButtonDown("Swap2");
sDown3 = Input.GetButtonDown("Swap3");
}
void Move()
{
// x y z
moveVec = new Vector3(hAxis, 0, vAxis).normalized;
// 회피 시 업데이트 안되게
if (isDodge)
moveVec = dodgeVec;
// 스왑 및 공격 시 못움직이게
if (isSwap || !isFireReady)
moveVec = Vector3.zero;
// transform + 벽뚫방지
if (!isBorder)
transform.position += moveVec * (rDown ? 1.3f : 1f) * speed * Time.deltaTime;
// animator
anim.SetBool("isWalk", moveVec != Vector3.zero);
anim.SetBool("isRun", rDown);
}
void Turn()
{
// #1. 키보드에 의한 회전
transform.LookAt(transform.position + moveVec);
// #2. 마우스에 의한 회전
// Ray란?, ScreenPointToRay() : 스크린에서 월드로 Ray를 쏘는 함수
if(fDown)
{
Ray ray = followCamera.ScreenPointToRay(Input.mousePosition);
RaycastHit rayHit;
// out 키워드는, 반환값을 주어진 변수에 저장하는 키워드
if (Physics.Raycast(ray, out rayHit, 100))
{
Vector3 nextVec = rayHit.point;
nextVec.y = 0;
transform.LookAt(nextVec);
}
}
}
void Jump()
{
if(jDown && moveVec == Vector3.zero && !isJump && !isDodge) {
// 물리엔진에 힘을 준다. 여기선 즉발적인 Impulse
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;
// out 키워드는, 반환값을 주어진 변수에 저장하는 키워드
if (Physics.Raycast(ray, out rayHit, 100))
{
Vector3 nextVec = rayHit.point;
nextVec.y = 15;
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) {
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 || equipWeapon.curAmmo == equipWeapon.maxAmmo)
return;
if(reDown && !isJump && !isDodge && !isSwap && isFireReady)
{
anim.SetTrigger("doReload");
isReload = true;
// Invoke("ReloadOut", 2f); -> 애니메이션 기능에 ReloadOut() 기능 구현했습니다.
}
}
public void ReloadOut()
{
int reAmmo = equipWeapon.maxAmmo - equipWeapon.curAmmo;
reAmmo = ammo < reAmmo ? ammo : reAmmo;
equipWeapon.curAmmo += reAmmo;
ammo -= reAmmo;
isReload = false;
}
void Dodge()
{
// 이동하면서 점프할때,
if (jDown && moveVec != Vector3.zero && !isJump && !isDodge)
{
dodgeVec = moveVec;
speed *= 2;
anim.SetTrigger("doDodge");
isDodge = true;
Invoke("DodgeOut", 0.5f); // 시간지연 라이브러리 기능
}
}
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;
// 1, 2 ,3 버튼 누를때
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()
{
// DrawRay() : Scene내에 Ray를 보여주는 함수
//Debug.DrawRay(transform.position, transform.forward * 5, Color.green);
isBorder = Physics.Raycast(transform.position, transform.forward, 3, LayerMask.GetMask("Wall"));
}
void FixedUpdate()
{
FreezeRotation();
StopToWall();
}
// 충돌 시 이벤트 함수로 착지 구현
void OnCollisionEnter(Collision collision)
{
if(collision.gameObject.tag == "Floor"){
isJump = false;
anim.SetBool("isJump", false);
}
}
// 아이템 획득
private 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;
break;
}
Destroy(other.gameObject);
}
else if (other.tag == "EnemyBullet") {
if (!isDamage) {
Bullet enemyBullet = other.GetComponent<Bullet>();
health -= enemyBullet.damage;
bool isBossAtk = other.name == "Boss Melee Area";
StartCoroutine(OnDamage(isBossAtk));
}
if (other.GetComponent<Rigidbody>() != null)
Destroy(other.gameObject);
}
}
IEnumerator OnDamage(bool isBossAtk)
{
isDamage = true;
foreach(MeshRenderer mesh in meshs) {
mesh.material.color = Color.yellow;
}
if (isBossAtk)
rigid.AddForce(transform.forward * -25, ForceMode.Impulse);
yield return new WaitForSeconds(1f);
isDamage = false;
foreach (MeshRenderer mesh in meshs)
{
mesh.material.color = Color.white;
}
if (isBossAtk)
rigid.velocity = Vector3.zero;
}
private void OnTriggerStay(Collider other)
{
if (other.tag == "Weapon")
nearObject = other.gameObject;
}
private void OnTriggerExit(Collider other)
{
if (other.tag == "Weapon")
nearObject = null;
}
}
적 스크립트 전체보기
더보기
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class Enemy : MonoBehaviour
{
public enum Type { A, B, C, D };
public Type enemyType;
public int maxHealth;
public int curHealth;
public Transform Target;
public BoxCollider melleArea;
public GameObject bullet;
public bool isChase;
public bool isAttack;
public bool isDead;
public Rigidbody rigid;
public BoxCollider boxCollider;
public MeshRenderer[] meshs;
public NavMeshAgent nav;
public Animator anim;
void Awake()
{
rigid = GetComponent<Rigidbody>();
boxCollider = GetComponent<BoxCollider>();
// Material은 Mesh Renderer 컴포넌트에서 접근가능합니다.
meshs = GetComponentsInChildren<MeshRenderer>();
// 네비게이션
nav = GetComponent<NavMeshAgent>();
//애니메이션
anim = GetComponentInChildren<Animator>();
if(enemyType != Type.D)
Invoke("ChaseStart", 2);
}
void ChaseStart()
{
isChase = true;
anim.SetBool("isWalk", true);
}
void Update()
{
if (nav.enabled&& enemyType != Type.D) {
nav.SetDestination(Target.position);
nav.isStopped = !isChase;
}
}
void FreezeVelocity()
{
if (isChase) {
rigid.velocity = Vector3.zero;
rigid.angularVelocity = Vector3.zero;
}
}
void Targeting()
{
if (!isDead && enemyType != Type.D)
{
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);
melleArea.enabled = true;
yield return new WaitForSeconds(1f);
melleArea.enabled = false;
break;
case Type.B:
yield return new WaitForSeconds(0.1f);
rigid.AddForce(transform.forward * 20, ForceMode.Impulse);
melleArea.enabled = true;
yield return new WaitForSeconds(0.5f);
rigid.velocity = Vector3.zero;
melleArea.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)
{
foreach (MeshRenderer mesh in meshs)
mesh.material.color = Color.red;
yield return new WaitForSeconds(0.1f);
if(curHealth > 0) {
foreach (MeshRenderer mesh in meshs)
mesh.material.color = Color.white;
}
else {
foreach (MeshRenderer mesh in meshs)
mesh.material.color = Color.gray;
gameObject.layer = 12;
isDead = true;
isChase = false;
nav.enabled = false;
anim.SetTrigger("doDie");
// 죽었을 시, 넉백
if (isGrenade)
{
reactVec = reactVec.normalized;
reactVec += Vector3.up * 3;
rigid.freezeRotation = false; // freezeRotation
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);
}
if(enemyType != Type.D)
Destroy(gameObject, 4);
}
}
}
보스 스크립트 전체보기
더보기
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class Boss : Enemy
{
public GameObject missile;
public Transform missilePortA;
public Transform missilePortB;
Vector3 lookVec; // 플레이어 움직임 예측
Vector3 tauntVec; // 찍어내리는 벡터
public bool isLook; // 플레이어 바라보는 플래그
void Awake()
{
rigid = GetComponent<Rigidbody>();
boxCollider = GetComponent<BoxCollider>();
meshs = GetComponentsInChildren<MeshRenderer>(); // Material은 Mesh Renderer 컴포넌트에서 접근가능합니다.
nav = GetComponent<NavMeshAgent>(); // 네비게이션
anim = GetComponentInChildren<Animator>(); //애니메이션
nav.isStopped = true;
StartCoroutine(Think());
}
void Update()
{
if (isDead){
StopAllCoroutines();
return;
}
if (isLook)
{
float h = Input.GetAxisRaw("Horizontal");
float v = Input.GetAxisRaw("Vertical");
lookVec = new Vector3(h, 0, v) * 5f;
transform.LookAt(Target.position + lookVec);
}
else
nav.SetDestination(tauntVec);
}
IEnumerator Think()
{
yield return new WaitForSeconds(0.1f);
int ranAction = Random.Range(0, 5); // 0 ~ 4
switch (ranAction)
{
case 0:
case 1:
// 미사일 발사 패턴
StartCoroutine(MissileShot());
break;
case 2:
case 3:
// 돌 굴러가는 패턴
StartCoroutine(RockShot());
break;
case 4:
// 점프 공격 패턴
StartCoroutine(Taunt());
break;
}
}
IEnumerator MissileShot()
{
anim.SetTrigger("doShot");
yield return new WaitForSeconds(0.2f);
GameObject instantMissileA = Instantiate(missile, missilePortA.position, missilePortA.rotation);
BossMissile bossMissileA = instantMissileA.GetComponent<BossMissile>();
bossMissileA.target = Target;
yield return new WaitForSeconds(0.3f);
GameObject instantMissileB = Instantiate(missile, missilePortB.position, missilePortB.rotation);
BossMissile bossMissileB = instantMissileB.GetComponent<BossMissile>();
bossMissileB.target = Target;
yield return new WaitForSeconds(2.5f);
StartCoroutine(Think());
}
IEnumerator RockShot()
{
isLook = false;
anim.SetTrigger("doBigShot");
Instantiate(bullet, transform.position, transform.rotation);
yield return new WaitForSeconds(3f);
isLook = true;
StartCoroutine(Think());
}
IEnumerator Taunt()
{
tauntVec = Target.position + lookVec;
isLook = false;
nav.isStopped = false;
boxCollider.enabled = false;
anim.SetTrigger("doTaunt");
yield return new WaitForSeconds(1.5f);
melleArea.enabled = true;
yield return new WaitForSeconds(0.5f);
melleArea.enabled = false;
yield return new WaitForSeconds(1f);
isLook = true;
nav.isStopped = true;
boxCollider.enabled = true;
StartCoroutine(Think());
}
}
🧷 1. 보스 오브젝트 및 애니메이션
- 보스 오브젝트 설정
- rigidbody 추가후, 쓰러짐 방지를 위해 Freeze Rotation X,Z 체크
- Box Collider 추가후, 콜라이더 영역 맞추기
- Nav Mesh Agent 추가, Angular Speed는 0으로 두었습니다.
- 보스 애니메이션 설정
- Enemy D 라는 새로운 애니메이션 컨트롤러 추가 후, 보스 오브젝트 하위 Mesh Object에 추가하였습니다.
- 공격 패턴인 BigShot, Shot, Taunt와 죽는 모션인 Die는 트리거로 설정합니다.
- Any State에서 애니메이션으로 가는 트랜잭션은 Has Exit Time을 체크해제하고, Transition Duration을 0으로 둡니다
- 애니메이션에서 Exit로 가는 트랜잭션은 Has Exit Time을 체크해 줍시다. Transition Duration은 0.1초로 설정합니다.
- 미사일 발사 위치
- 보스 공격패턴중 하나인 미사일 발사 위치 A, B를 위치에 맞게 양 귀쪽으로 설정해줍시다.
- 근접 공격 영역
- 보스 공격패턴중 점프해서 떨어지는 근접공격 패턴은, Box Collider와, Bullet 스크립트를 추가해 영역을 줍니다.
🧷 2. 미사일, 돌 공격 오브젝트
- 미사일 오브젝트
- 에셋의 프리팹에서 Missile Boss 프리팹을 가져옵니다.
- Effect로 Particle System을 주어 불꽃을 표현해 주었습니다.
- 여기서 중요한 점은 오브젝트의 forword 방향이 z축이기에 그에 맞게 Mesh Object를 회전시켜줍시다.
- Mesh Object에 Missile 스크립트를 장착시켜, 회전감을 줍니다.
- 부모인 Object안 컴포넌트로는 Box Collider인 피격 영역 , Nav Mesh Agent를 통한 유도기능을 추가합니다.
- Box Collider은 플레이어를 공격하고 사라지기에 isTrigger을 체크해줍시다.
- Nav Mesh Agent는 속도감을 위하여, Speed와, Agular Speed, Acceleration등을 입맛대로 설정합니다.
이후 Boss Missile 이라는 Bullet에서 상속받는 스크립트를 추가해줍니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class BossMissile : Bullet
{
public Transform target;
NavMeshAgent nav;
void Awake()
{
nav = GetComponent<NavMeshAgent>();
}
void Update()
{
nav.SetDestination(target.position);
}
}
- 보스 미사일에는 유도기능이 장착되어 있습니다.
- 돌 오브젝트
- 미사일 처럼 에셋에서 Boss Rock을 가져와, 컴포넌트를 추가합니다.
- RigidBody : Mass 10, Freez Rotation Y, Z (굴러서 공격하여, X축은 회전할 예정입니다.)
- Sphere Collider 한개는 물리효과를 위해, 하나는 플레이어 피격을 위한 is Trigger을 체크합니다.
이후 Boss Rock이라는 Bullet을 상속받은 스크립트를 추가합니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BossRock : Bullet
{
Rigidbody rigid;
float angularPower = 2f;
float scaleValue = 0.1f;
bool isShoot;
void Awake()
{
rigid = GetComponent<Rigidbody>();
StartCoroutine(GainPowerTimer());
StartCoroutine(GainPower());
}
IEnumerator GainPowerTimer()
{
yield return new WaitForSeconds(2.2f);
isShoot = true;
}
IEnumerator GainPower()
{
while (!isShoot) {
angularPower += 0.02f;
scaleValue += 0.005f;
transform.localScale = Vector3.one * scaleValue;
rigid.AddTorque(transform.right * angularPower, ForceMode.Acceleration);
yield return null;
}
}
}
- 코루틴을 이용한 GainPowerTimer() 함수는 돌이 발사하기까지 기를 모으는 함수입니다.
- 2.2초의 시간동안 모을 수 있씁니다.
- isShoot 플래그로 돌맹이의 발사준비를 알립니다.
- GainPower() 함수는 isShoot 플래그가 false인 경우에 지속적으로 회전력과, 크기를 키워줍니다.
public class Bullet : MonoBehaviour
{
public int damage;
public bool isMelee;
public bool isRock;
void OnCollisionEnter(Collision collision)
{
if(!isRock && collision.gameObject.tag == "Floor") {
Destroy(gameObject, 3);
}
}
void OnTriggerEnter(Collider other)
{
if (!isMelee && other.gameObject.tag == "Wall")
{
Destroy(gameObject);
}
}
}
- Bullet 스크립트에는, isRock 플래그를 두어, 돌이 바닥에 닿더라도 사라지지 않게하는 조건을 추가합니다.
🧷 3. 보스 기본로직
보스는 기본적으로 점프공격빼고 그 자리에 가만히 있습니다.
그리고 플레이어를 맞추기 쉽게 하기위해, 플레이어 움직임을 예측하는 AI 회전기능을 만듭니다.
- Boss 스크립트 상속을 위한 Enemy 스크립트 로직 정리 1
/*Enemy script, Boss 상속을 위해 고쳐줍시다. 수정된 부분만 담긴 스크립트 일부입니다.*/
public class Enemy : MonoBehaviour
{
public Rigidbody rigid;
public BoxCollider boxCollider;
public MeshRenderer[] meshs;
public NavMeshAgent nav;
public Animator anim;
}
- 보스 스트립트에서 사용하기 위해, public으로 바꿔 줍니다.
- Boss 스크립트 상속을 위한 Enemy 스크립트 로직 정리 2
/*Enemy script, Boss 상속을 위해 고쳐줍시다. 수정된 부분만 담긴 스크립트 일부입니다.*/
public class Enemy : MonoBehaviour
{
public enum Type { A, B, C, D };
public bool isDead;
void Awake()
{
if(enemyType != Type.D)
Invoke("ChaseStart", 2);
}
void Update()
{
if (nav.enabled && enemyType != Type.D) {
nav.SetDestination(Target.position);
nav.isStopped = !isChase;
}
}
void Targeting()
{
// 타게팅은 D가 아닐때, 몬스터가 죽지 않았을 때 를 기준으로 해줍니다.
if (!isDead && enemyType != Type.D) {}
}
IEnumerator OnDamage(Vector3 reactVec, bool isGrenade)
{
if(curHealth > 0) {
}
else {
// 죽었을 시
if(enemyType != Type.D)
Destroy(gameObject, 4);
}
}
}
- ChaseStart() 기능은 보스가 아닌 경우에만 실행
- Update() 구문의 navigate 설정부분도, 보스가 아닌 경우에만
- Targeting() 구문도, 보스가 아닌 경우에
- OnDamage()에서 몬스터가 파괴되는 경우도, 보스는 파괴되지 않게 해줍시다.
- Enemy 피격 시 색변경 로직 살짝 바꿔주기
/*Enemy script, 수정된 부분만 담긴 스크립트 일부입니다.*/
public class Enemy : MonoBehaviour
{
public MeshRenderer[] meshs;
void Awake()
{
// Material은 Mesh Renderer 컴포넌트에서 접근가능합니다.
meshs = GetComponentsInChildren<MeshRenderer>();
}
IEnumerator OnDamage(Vector3 reactVec, bool isGrenade)
{
foreach (MeshRenderer mesh in meshs)
mesh.material.color = Color.red;
if(curHealth > 0) {
foreach (MeshRenderer mesh in meshs)
mesh.material.color = Color.white;
}
else {
foreach (MeshRenderer mesh in meshs)
mesh.material.color = Color.gray;
}
}
}
- forEach 문을 사용하여 적 피격시 모든 메쉬의 material들의 컬러를 바꿔줍니다.
- Boss 스크립트 기본 로직
/*Boss script */
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class Boss : Enemy
{
public GameObject missile;
public Transform missilePortA;
public Transform missilePortB;
Vector3 lookVec; // 플레이어 움직임 예측
Vector3 tauntVec; // 찍어내리는 벡터
public bool isLook; // 플레이어 바라보는 플래그
void Awake()
{
rigid = GetComponent<Rigidbody>();
boxCollider = GetComponent<BoxCollider>();
meshs = GetComponentsInChildren<MeshRenderer>(); // Material은 Mesh Renderer 컴포넌트에서 접근가능합니다.
nav = GetComponent<NavMeshAgent>(); // 네비게이션
anim = GetComponentInChildren<Animator>(); //애니메이션
nav.isStopped = true;
StartCoroutine(Think());
}
void Update()
{
if (isDead){
StopAllCoroutines();
return;
}
if (isLook)
{
float h = Input.GetAxisRaw("Horizontal");
float v = Input.GetAxisRaw("Vertical");
lookVec = new Vector3(h, 0, v) * 5f;
transform.LookAt(Target.position + lookVec);
}
else
nav.SetDestination(tauntVec);
}
IEnumerator Think(){}
IEnumerator MissileShot() {}
IEnumerator RockShot(){}
IEnumerator Taunt() {}
}
- 상속시 Awake() 함수는 자식 스크립트 단독 실행이므로, 보스 스크립트에서 Enemy의 스크립트에서 public으로 만들어 둔 rigid, boxCollider, meshs, nav, anim에 초기화 시켜줍시다.
- Update() 구문에선, Boss가 바라보는 방향을 플레이어가 가려는 값을 넣은 lookVec를 만들어주었습니다.
- 이로인해 플레이어가 공격을 피하기 쉽지 않게 될 예정입니다.
- 죽을 시 모든 공격 코루틴은 멈춥니다.
🧷 4. 보스 공격 패턴 로직
- 공격 패턴 무작위 기능
/*Boss script */
public class Boss : Enemy
{
IEnumerator Think()
{
yield return new WaitForSeconds(0.1f);
int ranAction = Random.Range(0, 5); // 0 ~ 4
switch (ranAction)
{
case 0:
case 1:
// 미사일 발사 패턴
StartCoroutine(MissileShot());
break;
case 2:
case 3:
// 돌 굴러가는 패턴
StartCoroutine(RockShot());
break;
case 4:
// 점프 공격 패턴
StartCoroutine(Taunt());
break;
}
}
IEnumerator MissileShot() {}
IEnumerator RockShot(){}
IEnumerator Taunt() {}
}
- Random 함수를 사용하여, 확률별로 어떤 공격 패턴을 사용할지 정해줍니다.
- 미사일 공격 패턴
/*Boss script */
public class Boss : Enemy
{
IEnumerator MissileShot()
{
anim.SetTrigger("doShot");
yield return new WaitForSeconds(0.2f);
GameObject instantMissileA = Instantiate(missile, missilePortA.position, missilePortA.rotation);
BossMissile bossMissileA = instantMissileA.GetComponent<BossMissile>();
bossMissileA.target = Target;
yield return new WaitForSeconds(0.3f);
GameObject instantMissileB = Instantiate(missile, missilePortB.position, missilePortB.rotation);
BossMissile bossMissileB = instantMissileB.GetComponent<BossMissile>();
bossMissileB.target = Target;
yield return new WaitForSeconds(2.5f);
StartCoroutine(Think());
}
IEnumerator RockShot(){}
IEnumerator Taunt() {}
}
- 애니메이션 트리거를 통하여, 애니메이션 동작을 하게 해줍니다
- 미리 만든 Boss Missile을 인스턴트화하여 타겟만 정해줘, 미사일이 플레이어를 따라오게 만들어줍니다.
- 이후 다음 공격 패턴을 위해 Think() 코루틴을 호출해 줍니다.
- 돌 공격 패턴
/*Boss script */
public class Boss : Enemy
{
IEnumerator RockShot()
{
isLook = false;
anim.SetTrigger("doBigShot");
Instantiate(bullet, transform.position, transform.rotation);
yield return new WaitForSeconds(3f);
isLook = true;
StartCoroutine(Think());
}
IEnumerator Taunt() {}
}
- 돌 공격은 보스가 회전 움직임을 멈춰서 쏘기에 isLook 트리거를 false로 두었습니다
- 이후 애니메이션 트리거를 동작
- 돌은 인스턴트화 해주기만 하여도, BossRock 자체에서 굴러가는게 구현이 되어었습니다.
- 점프 공격 패턴
/*Boss script */
public class Boss : Enemy
{
IEnumerator Taunt()
{
tauntVec = Target.position + lookVec;
isLook = false;
nav.isStopped = false;
boxCollider.enabled = false;
anim.SetTrigger("doTaunt");
yield return new WaitForSeconds(1.5f);
melleArea.enabled = true;
yield return new WaitForSeconds(0.5f);
melleArea.enabled = false;
yield return new WaitForSeconds(1f);
isLook = true;
nav.isStopped = true;
boxCollider.enabled = true;
StartCoroutine(Think());
}
}
- tauntVec = 플레이어 위치 + lookVec(플레이어 움직임 예측) 을 넣어줍시다
- 꺼두었던 navigate는 nav.isStopped =false로 다시 켜주고, 처음엔 박스콜라이더를 비활성화시킵니다.
- 이후, 코루틴을 통해 1.5초 이후에 콜라이더를 활성화시키고, 0.5초 이후에는 꺼줍니다.
- 이후 isLook, nav.isStopped, boxCollider까지 true로 제 위치를 시켜줍니다.
🧷 5. 마지막 오브젝트 점검 및 플레이어 넉백
- 오브젝트 점검
- Boss 스크립트에는 각각에 맞게 오브젝트들이 들어가있어야합니다.
- Target = Player
- Melle Area = Boss Melee Area
- Bullet = Boss Rock
- Missile = Missile Boss
- Missile Port A,B = Missile Port A,B
- isLook 체크 (바라보는게 디폴트값으로 되어있어야합니다.)
- 각 공격에 대한 오브젝트 Tag, Layer을 EnemyBullet으로 맞춰야합니다
- 또 Bullet 스크립트는 각각에 맞게 체크해주고, 데미지를 넣어줍시다.
- 플레이어 넉백기능
/* Player script , 추가되는 부분만 작성하였습니다. */
private void OnTriggerEnter(Collider other)
{
if(other.tag == "Item") {}
else if (other.tag == "EnemyBullet") {
if (!isDamage) {
bool isBossAtk = other.name == "Boss Melee Area";
StartCoroutine(OnDamage(isBossAtk));
}
if (other.GetComponent<Rigidbody>() != null)
Destroy(other.gameObject);
}
}
IEnumerator OnDamage(bool isBossAtk)
{
if (isBossAtk)
rigid.AddForce(transform.forward * -25, ForceMode.Impulse);
yield return new WaitForSeconds(1f);
if (isBossAtk)
rigid.velocity = Vector3.zero;
}
- 플레이어 OnDamage 함수에 매개변수로 isBossAtk라는 bool 값을 넘겨주어 플레이어에게 넉백기능을 줍니다.
- 소소한 고치기, 미사일 한개 맞을 시 , 다른 맞는 공격들은 다 제거해줍시다.
if (other.GetComponent<Rigidbody>() != null)
Destroy(other.gameObject);
후후~ 이번 강의에서는 보스를 만들면서, 보스의 공격 패턴 몇가지를 배우게 됐습니다.
이를 이용해서 플레이어의 스킬도 충분히 만들어 볼 수 있겠다고 느끼고, 상속에 대해서 더 이해하기 쉽게 사용했습니다.
출처: 골든메탈님 유튜브
https://www.youtube.com/watch?v=7JlujO3JYas&list=PLO-mt5Iu5TeYkrBzWKuTCl6IUm_bA6BKy&index=13
'🎮 Game Dev (게임개발) > PC (데스크탑, 노트북, 터치패널)' 카테고리의 다른 글
[3D 액션게임] 15. 상점 만들기 (0) | 2022.04.11 |
---|---|
[3D 액션게임] 14. UI 배치하기 (0) | 2022.04.09 |
[3D 액션게임] 12. 다양한 몬스터 만들기 (0) | 2022.04.07 |
[3D 액션게임] 11. 목표를 추적하는 AI 만들기 (0) | 2022.04.07 |
[3D 액션게임] 10. 수류탄 구현하기 (0) | 2022.04.06 |
Contents
소중한 공감 감사합니다