Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
Tags
- 자료 구조
- 2024년
- 게임 엔진 공부
- 유니티 심화과정
- 기초
- 2023년
- 4월
- 단계별로 풀어보기
- 골드메탈
- 프로그래머스
- 코딩 기초 트레이닝
- 유니티
- c++
- 다이나믹 프로그래밍
- 2025년
- 10월
- 2022년
- 5월
- 2월
- 3월
- 1월
- 코딩 테스트
- 백준
- 수학
- todolist
- 개인 프로젝트 - 런앤건
- 개인 프로젝트
- 6월
- 입문
- C/C++
Archives
- Today
- Total
기록 보관소
[Unity/유니티] 기초-3D 쿼터뷰 액션 게임: 다양한 패턴을 구사하는 보스 만들기[B50] 본문
유니티 프로젝트/3D 쿼터뷰 액션게임
[Unity/유니티] 기초-3D 쿼터뷰 액션 게임: 다양한 패턴을 구사하는 보스 만들기[B50]
JongHoon 2022. 4. 7. 00:16개요
유니티 입문과 독학을 위해서 아래 링크의 골드메탈님의 영상들을 보며 진행 상황 사진 또는 캡처를 올리고 배웠던 점을 요약해서 적는다.
현재는 영상들을 보고 따라하고 배우는 것에 집중할 것이며, 영상을 모두 보고 따라한 후에는 개인 프로젝트를 설계하고 직접 만드는 것이 목표다.
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 쿼터뷰 액션 게임: 다양한 패턴을 구사하는 보스 만들기[B50]
1. 보스 기본 세팅




- Transition은 이전 애니메이션 작업들과 같다. AnyState -> @는 Transition Duration을 0, Has Exit Time을 체크 해제하고, @ -> Exit은 Transition Duration만 0.1로 변경해준다.




- 애니메이션 작동 테스트를 위해 실행해보니 크기가 작아지는 문제가 있었다. 그래서 댓글을 찾아보니 이는 애니메이션을 통해 Scale이 고정되는 문제이므로, Enemy D의 MeshArea가 아닌 Enemy D의 Scale을 3으로 바꿔주면 게임 실행시 문제 없을 것이라고 해서 이후 파트 진행하면서 중간에 바꿔주었다.
2. 투사체(미사일) 만들기




- 그런데 Bullet 스크립트는 분명 보스의 미사일에 기능에 필요하지만, 유도 미사일로서의 기능을 추가하기에는 기존에 Bullet 스크립트 파일을 사용하는 오브젝트 전체에 영향이 가므로 무리가 있다. 따라서, 새로운 스크립트를 만들어 Bullet을 상속받는 형식으로 진행한다.

//BossMissile 스크립트 파일
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);
}
}




3. 투사체(바위) 만들기

//BossRock 스크립트 파일
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BossRock : Bullet {
Rigidbody rigid;
float angularPower = 2;
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); //Acceleration : 가속도 형태?
yield return null; //while문 안에 넣지 않으면 게임 정지 문제 발생 가능
}
}
}
- 미사일과 마찬가지의 이유로 Bullet을 상속받도록 한다.
//Bullet 스크립트 파일
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
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); //3초 뒤에 사라지기
}
void OnTriggerEnter(Collider other) { //총알을 isTrigger로 바꾸었으므로
if(!isMelee && other.gameObject.tag == "Wall") //총알
Destroy(gameObject);
}
}
- 보스가 패턴으로 사용한 것은 3초 뒤에 사라지면 안되니까 Bullet 스크립트에 이를 구별하는 bool형 변수를 추가해 조건으로 이를 막는다.

- SphereCollider를 2개로 한 이유는 하나는 트리거로 이용해서 벽이나 플레이어에 닿았을때 사라지게 하기위함이다.





+) 추가
- 위 실행화면을 보면 보이듯 바위가 원본 크기보다 훨씬 커진다. 그래서 간단하게 코루틴의 while문에 scaleValue에 제한을 거는 if문을 추가해서 1이하면 scale을 증가시키고, 아니면 증가시키지 않도록 만들어서 해결했다.
//BossRock 스크립트 파일
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BossRock : Bullet {
Rigidbody rigid;
float angularPower = 2;
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;
if (scaleValue < 1)
transform.localScale = Vector3.one * scaleValue;
rigid.AddTorque(transform.right * angularPower, ForceMode.Acceleration); //Acceleration : 가속도 형태?
yield return null; //while문 안에 넣지 않으면 게임 정지 문제 발생 가능
}
}



4. 보스 로직 준비
//Enemy 스크립트 파일
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 meleeArea;
public GameObject bullet;
public bool isChase;
public bool isAttack;
Rigidbody rigid;
BoxCollider boxCollider;
MeshRenderer[] meshs;
NavMeshAgent nav;
Animator anim;
void Awake() {
rigid = GetComponent<Rigidbody>();
boxCollider = GetComponent<BoxCollider>();
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 (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);
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) {
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; //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);
}
if(enemyType != Type.D)
Destroy(gameObject, 4);
}
}
}
- 보스 또한 Enemy 스크립트 파일을 상속받을 것이므로, 보스 타입인 D를 배열에 추가해주고 이 보스에 맞지 않는 일부 동작들(Chase 등)을 조건을 달아서 보스에게는 작동하지 않도록 수정해주었다.
- 추가로 보스와 이전 시간에 만들었던 Enemy C의 경우, 여러가지 Material을 사용해서 피격시 일부분만 색상에 변화가 생기게 된다. 그래서 플레이어처럼 MeshRenderer를 통해서 모든 Material이 변경되도록 수정해주었다.


5. 기본 로직
//Boss 스크립트 파일
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Boss : Enemy {
public GameObject missile;
public Transform missilePortA;
public Transform missilePortB;
Vector3 lookVec;
Vector3 tauntVec;
bool isLook;
void Start() {
isLook = true;
}
void Update() {
if (isLook) {
float h = Input.GetAxisRaw("Horizontal");
float v = Input.GetAxisRaw("Vertical");
lookVec = new Vector3(h, 0, v) * 5f; //플레이어의 입력값으로 예측 벡터값 생성
transform.LookAt(target.position + lookVec);
}
}
}
- 주석 설명처럼 플레이어 입력 값을 통해서 해당 값 조금 더 앞의 예측 벡터값을 생성해서 보스가 그곳을 바라보도록 만들었다.



6. 패턴 로직
//Enemy 스크립트 파일
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 meleeArea;
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>();
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);
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) {
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; //Enemy Dead 레이어로 변경
isDead = true;
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);
}
if(enemyType != Type.D)
Destroy(gameObject, 4);
}
}
}
//Boss 스크립트 파일
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;
bool isLook;
void Awake() {
rigid = GetComponent<Rigidbody>();
boxCollider = GetComponent<BoxCollider>();
meshs = GetComponentsInChildren<MeshRenderer>();
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); //확률 설정
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(2f);
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);
meleeArea.enabled = true;
yield return new WaitForSeconds(0.5f);
meleeArea.enabled = false;
yield return new WaitForSeconds(1f);
isLook = true;
nav.isStopped = true;
boxCollider.enabled = true;
StartCoroutine(Think());
}
}
- Awake() 함수는 자식 스크립트만 단독 실행된다. 따라서 Enemy 스크립트에서 Rigidbody 등을 public으로 전환하고 Awake()에서 초기화를 해주더라도 Boss에서는 실행되지 않게된다. 그래서 Awake()를 Start()로 바꾸거나, 위 코드처럼 Boss 스크립트에서도 Awake()에서 초기화해주는 방식으로 해결할 수 있다.
7. 로직 점검

- 점프 공격 범위인 Melee Area에 Bullet 스크립트 파일이 없어서 플레이어가 EnemyBullet에 맞았는데도 피해량 등을 계산하지 못해서 오류가 뜨는 문제가 있었다. 그래서 스크립트 파일을 추가해서 이를 해결해주었다.
//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;
bool isBossAtk = other.name == "Boss Melee Area";
StartCoroutine(OnDamage(isBossAtk));
}
if (other.GetComponent<Rigidbody>() != null) //Rigidbody 유무를 판단(미사일)
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;
}
void OnTriggerStay(Collider other) {
if (other.tag == "Weapon")
nearObject = other.gameObject;
}
void OnTriggerExit(Collider other) {
if (other.tag == "Weapon")
nearObject = null;
}
}
- Player 스크립트에서 플레이어가 피해를 받은 직후 미사일에 맞게되면 이 미사일이 없어지지 않는 문제가 있어서 EnemyBullet을 맞았을 때(!isDamage) 조건문 안에 있는 Destroy() 조건문을 바깥으로 옮겨서 해결하였다.
- 또한, 보스가 점프 공격 후 보스 Collider가 돌아오면서 플레이어가 멀리 튕겨나가는 문제가 있어서, 보스의 Collider에 닿기 전에 플레이어가 뒤로 튕겨나가는 로직을 추가해서 이를 해결해주었다. 또한 좀 더 직관성을 위해서 위의 보스 점프 공격 범위인 Melee Area를 Boss Melee Area로 이름을 변경해주었다.





'유니티 프로젝트 > 3D 쿼터뷰 액션게임' 카테고리의 다른 글
[Unity/유니티] 기초-3D 쿼터뷰 액션 게임: 간단한 상점 만들기[B52] (0) | 2022.04.09 |
---|---|
[Unity/유니티] 기초-3D 쿼터뷰 액션 게임: UI 배치하기 [B51] (0) | 2022.04.08 |
[Unity/유니티] 기초-3D 쿼터뷰 액션 게임: 다양한 몬스터 만들기[B49] (0) | 2022.04.03 |
[Unity/유니티] 기초-3D 쿼터뷰 액션 게임: 목표를 추적하는 AI 만들기[B48] (0) | 2022.04.02 |
[Unity/유니티] 기초-3D 쿼터뷰 액션 게임: 수류탄 구현하기[B47] (0) | 2022.04.02 |