일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- C/C++
- c++
- 프로그래머스
- 코딩 기초 트레이닝
- 1월
- 코딩 테스트
- 10월
- 2023년
- 자료 구조
- 2022년
- 게임 엔진 공부
- 유니티
- 개인 프로젝트
- 기초
- 다이나믹 프로그래밍
- 3월
- 2월
- 입문
- 5월
- 4월
- 골드메탈
- 2024년
- 단계별로 풀어보기
- 2025년
- 유니티 심화과정
- 수학
- 백준
- todolist
- 6월
- 개인 프로젝트 - 런앤건
- Today
- Total
기록 보관소
[Unity/유니티] 기초-3D 쿼터뷰 액션 게임: 다양한 몬스터 만들기[B49] 본문
개요
유니티 입문과 독학을 위해서 아래 링크의 골드메탈님의 영상들을 보며 진행 상황 사진 또는 캡처를 올리고 배웠던 점을 요약해서 적는다.
현재는 영상들을 보고 따라하고 배우는 것에 집중할 것이며, 영상을 모두 보고 따라한 후에는 개인 프로젝트를 설계하고 직접 만드는 것이 목표다.
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;
}
}
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 스크립트 파일
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);
}
}
}
4. 돌격형 몬스터
- 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);
}
}
}
5. 원거리형 몬스터
//Missile 스크립트 파일
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Missile : MonoBehaviour {
void Update() {
transform.Rotate(Vector3.right * 30 * Time.deltaTime);
}
}
//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이므로 벽에 닿았을때 사라지는 것을 방지하기위해 수정 및 추가하였다.
'유니티 프로젝트 > 3D 쿼터뷰 액션게임' 카테고리의 다른 글
[Unity/유니티] 기초-3D 쿼터뷰 액션 게임: UI 배치하기 [B51] (0) | 2022.04.08 |
---|---|
[Unity/유니티] 기초-3D 쿼터뷰 액션 게임: 다양한 패턴을 구사하는 보스 만들기[B50] (0) | 2022.04.07 |
[Unity/유니티] 기초-3D 쿼터뷰 액션 게임: 목표를 추적하는 AI 만들기[B48] (0) | 2022.04.02 |
[Unity/유니티] 기초-3D 쿼터뷰 액션 게임: 수류탄 구현하기[B47] (0) | 2022.04.02 |
[Unity/유니티] 기초-3D 쿼터뷰 액션 게임: 피격 테스터 만들기[B46] (0) | 2022.03.30 |