일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 | 31 |
- 2024년
- 게임 엔진 공부
- 수학
- 다이나믹 프로그래밍
- 유니티 심화과정
- 코딩 테스트
- 기초
- 자료 구조
- 5월
- 프로그래머스
- 개인 프로젝트 - 런앤건
- 백준
- C/C++
- 입문
- c++
- 개인 프로젝트
- 3월
- 1월
- 2월
- 10월
- 유니티
- 7월
- todolist
- 코딩 기초 트레이닝
- 골드메탈
- 2023년
- 4월
- 단계별로 풀어보기
- 2022년
- 2025년
- Today
- Total
기록 보관소
[Unity/유니티] 기초-3D 쿼터뷰 액션 게임: 게임 완성하기[BE5] 본문
개요
유니티 입문과 독학을 위해서 아래 링크의 골드메탈님의 영상들을 보며 진행 상황 사진 또는 캡처를 올리고 배웠던 점을 요약해서 적는다.
현재는 영상들을 보고 따라하고 배우는 것에 집중할 것이며, 영상을 모두 보고 따라한 후에는 개인 프로젝트를 설계하고 직접 만드는 것이 목표다.
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 쿼터뷰 액션 게임: 게임 완성하기[BE5]
1. 스테이지 관리
//GameManager 스크립트 파일
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class GameManager : MonoBehaviour {
public GameObject menuCam;
public GameObject gameCam;
public Player player;
public Boss boss;
public GameObject itemShop;
public GameObject weaponShop;
public GameObject startZone;
public int stage;
public float playTime;
public bool isBattle;
public int enemyCntA;
public int enemyCntB;
public int enemyCntC;
public GameObject menuPanel;
public GameObject gamePanel;
public Text maxScoreTxt;
public Text scoreTxt;
public Text stageTxt;
public Text playTimeTxt;
public Text playerHealthTxt;
public Text playerAmmoTxt;
public Text playerCoinTxt;
public Image weapon1Img;
public Image weapon2Img;
public Image weapon3Img;
public Image weaponRImg;
public Text enemyATxt;
public Text enemyBTxt;
public Text enemyCTxt;
public RectTransform bossHealthGroup;
public RectTransform bossHealthBar;
void Awake() {
maxScoreTxt.text = string.Format("{0:n0}", PlayerPrefs.GetInt("MaxScore")); //세자리 수마다 , 추가
}
public void GameStart() {
menuCam.SetActive(false);
gameCam.SetActive(true);
menuPanel.SetActive(false);
gamePanel.SetActive(true);
player.gameObject.SetActive(true);
}
public void StageStart() {
itemShop.SetActive(false);
weaponShop.SetActive(false);
startZone.SetActive(false);
isBattle = true;
StartCoroutine(InBattle());
}
public void StageEnd() {
player.transform.position = Vector3.up * 0.8f;
itemShop.SetActive(true);
weaponShop.SetActive(true);
startZone.SetActive(true);
isBattle = false;
stage++;
}
IEnumerator InBattle() {
yield return new WaitForSeconds(5);
StageEnd();
}
void Update() {
if (isBattle)
playTime += Time.deltaTime;
}
void LateUpdate() {
//좌측 상단 UI
scoreTxt.text = string.Format("{0:n0}", player.score);
//우측 상단 UI
stageTxt.text = "STAGE " + stage;
int hour = (int)(playTime / 3600); //시
int min = (int)((playTime - hour * 3600) / 60); //분
int second = (int)(playTime % 60); //초
playTimeTxt.text = string.Format("{0:00}", hour) + ":" + string.Format("{0:00}", min) + ":" + string.Format("{0:00}", second);
//플레이어(좌측 하단) UI
playerHealthTxt.text = player.health + " / " + player.maxHealth;
playerCoinTxt.text = string.Format("{0:n0}", player.coin);
if (player.equipWeapon == null)
playerAmmoTxt.text = "- / " + player.ammo;
else if (player.equipWeapon.type == Weapon.Type.Melee)
playerAmmoTxt.text = "- / " + player.ammo;
else
playerAmmoTxt.text = player.equipWeapon.curAmmo + " / " + player.ammo;
//무기(중앙 하단) UI
weapon1Img.color = new Color(1, 1, 1, (player.hasWeapons[0] ? 1 : 0));
weapon2Img.color = new Color(1, 1, 1, (player.hasWeapons[1] ? 1 : 0));
weapon3Img.color = new Color(1, 1, 1, (player.hasWeapons[2] ? 1 : 0));
weaponRImg.color = new Color(1, 1, 1, (player.hasGrenades > 0 ? 1 : 0));
//몬스터 숫자(우측 하단) UI
enemyATxt.text = enemyCntA.ToString();
enemyBTxt.text = enemyCntB.ToString();
enemyCTxt.text = enemyCntC.ToString();
//보스 체력(중앙 상단) UI
bossHealthBar.localScale = new Vector3((float)boss.curHealth / boss.maxHealth, 1, 1);
}
}
- StartZone에 닿으면 스테이지를 시작하도록 StageStart와 StageEnd 함수, 그리고 테스트를 위한 5초를 주는 코루틴을 추가하고, 또한 스테이지 시작시 앞에서 만들었던 StartZone과 아이템 상점, 무기 상점은 비활성화 되어야하므로 변수로 추가해주었다.
//StartZone 스크립트 파일
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class StartZone : MonoBehaviour {
public GameManager manager;
void OnTriggerEnter(Collider other) {
if (other.gameObject.tag == "Player")
manager.StageStart();
}
}
2. 몬스터 프리펩
//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 int score;
public Transform target;
public BoxCollider meleeArea;
public GameObject bullet;
public GameObject[] coins;
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;
if (curHealth > 0) {
yield return new WaitForSeconds(0.1f);
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");
Player player = target.GetComponent<Player>();
player.score += score;
int ranCoin = Random.Range(0, 3);
Instantiate(coins[ranCoin], transform.position, Quaternion.identity);
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 스크립트에 없던 score와 떨어트릴 동전 변수를 추가해서 처치시 플레이어가 점수를 얻고, 동전 3종류 중 하나를 떨어트리도록 변경하였다. 그리고 스테이지 문제가 있으니 보스 시체 또한 4초 뒤 사라지도록 변경했다.
3. 몬스터 관리
//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 int score;
public GameManager manager;
public Transform target;
public BoxCollider meleeArea;
public GameObject bullet;
public GameObject[] coins;
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;
if (curHealth > 0) {
yield return new WaitForSeconds(0.1f);
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");
Player player = target.GetComponent<Player>();
player.score += score;
int ranCoin = Random.Range(0, 3);
Instantiate(coins[ranCoin], transform.position, Quaternion.identity);
switch(enemyType) {
case Type.A:
manager.enemyCntA--;
break;
case Type.B:
manager.enemyCntB--;
break;
case Type.C:
manager.enemyCntC--;
break;
case Type.D:
manager.enemyCntD--;
break;
}
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);
}
}
}
//GameManager 스크립트 파일
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class GameManager : MonoBehaviour {
public GameObject menuCam;
public GameObject gameCam;
public Player player;
public Boss boss;
public GameObject itemShop;
public GameObject weaponShop;
public GameObject startZone;
public int stage;
public float playTime;
public bool isBattle;
public int enemyCntA;
public int enemyCntB;
public int enemyCntC;
public int enemyCntD;
public Transform[] enemyZones;
public GameObject[] enemies;
public List<int> enemyList;
public GameObject menuPanel;
public GameObject gamePanel;
public Text maxScoreTxt;
public Text scoreTxt;
public Text stageTxt;
public Text playTimeTxt;
public Text playerHealthTxt;
public Text playerAmmoTxt;
public Text playerCoinTxt;
public Image weapon1Img;
public Image weapon2Img;
public Image weapon3Img;
public Image weaponRImg;
public Text enemyATxt;
public Text enemyBTxt;
public Text enemyCTxt;
public RectTransform bossHealthGroup;
public RectTransform bossHealthBar;
void Awake() {
enemyList = new List<int>();
maxScoreTxt.text = string.Format("{0:n0}", PlayerPrefs.GetInt("MaxScore")); //세자리 수마다 , 추가
}
public void GameStart() {
menuCam.SetActive(false);
gameCam.SetActive(true);
menuPanel.SetActive(false);
gamePanel.SetActive(true);
player.gameObject.SetActive(true);
}
public void StageStart() {
itemShop.SetActive(false);
weaponShop.SetActive(false);
startZone.SetActive(false);
foreach (Transform zone in enemyZones)
zone.gameObject.SetActive(true);
isBattle = true;
StartCoroutine(InBattle());
}
public void StageEnd() {
player.transform.position = Vector3.up * 0.8f;
itemShop.SetActive(true);
weaponShop.SetActive(true);
startZone.SetActive(true);
foreach (Transform zone in enemyZones)
zone.gameObject.SetActive(false);
isBattle = false;
stage++;
}
IEnumerator InBattle() {
if (stage % 5 == 0) {
enemyCntD++;
GameObject instantEnemy = Instantiate(enemies[3], enemyZones[0].position, enemyZones[0].rotation);
Enemy enemy = instantEnemy.GetComponent<Enemy>();
enemy.target = player.transform;
enemy.manager = this; //GameManager 보내주기
boss = instantEnemy.GetComponent<Boss>();
}
else {
for (int index = 0; index < stage; index++) {
int ran = Random.Range(0, 3);
enemyList.Add(ran);
switch (ran) {
case 0:
enemyCntA++;
break;
case 1:
enemyCntB++;
break;
case 2:
enemyCntC++;
break;
}
}
while (enemyList.Count > 0) {
int ranZone = Random.Range(0, 4);
GameObject instantEnemy = Instantiate(enemies[enemyList[0]], enemyZones[ranZone].position, enemyZones[ranZone].rotation);
Enemy enemy = instantEnemy.GetComponent<Enemy>();
enemy.target = player.transform;
enemy.manager = this; //GameManager 보내주기
enemyList.RemoveAt(0);
yield return new WaitForSeconds(4f);
}
}
while(enemyCntA + enemyCntB + enemyCntC + enemyCntD > 0) {
yield return null;
}
yield return new WaitForSeconds(4f);
boss = null;
StageEnd();
}
void Update() {
if (isBattle)
playTime += Time.deltaTime;
}
void LateUpdate() {
//좌측 상단 UI
scoreTxt.text = string.Format("{0:n0}", player.score);
//우측 상단 UI
stageTxt.text = "STAGE " + stage;
int hour = (int)(playTime / 3600); //시
int min = (int)((playTime - hour * 3600) / 60); //분
int second = (int)(playTime % 60); //초
playTimeTxt.text = string.Format("{0:00}", hour) + ":" + string.Format("{0:00}", min) + ":" + string.Format("{0:00}", second);
//플레이어(좌측 하단) UI
playerHealthTxt.text = player.health + " / " + player.maxHealth;
playerCoinTxt.text = string.Format("{0:n0}", player.coin);
if (player.equipWeapon == null)
playerAmmoTxt.text = "- / " + player.ammo;
else if (player.equipWeapon.type == Weapon.Type.Melee)
playerAmmoTxt.text = "- / " + player.ammo;
else
playerAmmoTxt.text = player.equipWeapon.curAmmo + " / " + player.ammo;
//무기(중앙 하단) UI
weapon1Img.color = new Color(1, 1, 1, (player.hasWeapons[0] ? 1 : 0));
weapon2Img.color = new Color(1, 1, 1, (player.hasWeapons[1] ? 1 : 0));
weapon3Img.color = new Color(1, 1, 1, (player.hasWeapons[2] ? 1 : 0));
weaponRImg.color = new Color(1, 1, 1, (player.hasGrenades > 0 ? 1 : 0));
//몬스터 숫자(우측 하단) UI
enemyATxt.text = enemyCntA.ToString();
enemyBTxt.text = enemyCntB.ToString();
enemyCTxt.text = enemyCntC.ToString();
//보스 체력(중앙 상단) UI
if (boss != null) {
bossHealthGroup.anchoredPosition = Vector3.down * 30;
bossHealthBar.localScale = new Vector3((float)boss.curHealth / boss.maxHealth, 1, 1);
}
else {
bossHealthGroup.anchoredPosition = Vector3.up * 200;
}
}
}
- 앞에서 만들었던 지역에서 적들이 생성되게 만들고, 또한 적들을 프리펩으로 만들어서 Enemy 스크립트에서 지정했던 일부 변수들을 GameManager를 통해서 지정해주었고, 보스전이 아닐 때 보스 체력 UI가 보이지 않도록 바꾸었다.
4. 게임 오버
- Die 애니메이션은 죽으면 플레이어 또한 더이상 활동을 못하게 할 것이므로 Exit에 Transition을 연결하지 않는다.
//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 GameManager manager;
public int ammo;
public int coin;
public int health;
public int score;
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;
bool isShop;
bool isDead = false;
Vector3 moveVec;
Vector3 dodgeVec;
Animator anim;
Rigidbody rigid;
MeshRenderer[] meshs;
GameObject nearObject;
public 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.GetButtonDown("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 || isDead) //무기 교체, 재장전, 공격 중, 사망일때는
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 && !isDead) {
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 && !isDead) { //움직이지 않고 점프
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 && !isDead) {
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 && !isShop && !isDead) {
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 && !isShop && !isDead) {
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 && !isDead) { //이동하면서 점프
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 && !isDead) {
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 && !isDead) {
if (nearObject.tag == "Weapon") {
Item item = nearObject.GetComponent<Item>();
int weaponIndex = item.value;
hasWeapons[weaponIndex] = true;
Destroy(nearObject);
}
else if(nearObject.tag == "Shop") {
Shop shop = nearObject.GetComponent<Shop>();
shop.Enter(this);
isShop = true;
}
}
}
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);
if (health <= 0 && !isDead)
OnDie();
yield return new WaitForSeconds(1f);
isDamage = false;
foreach (MeshRenderer mesh in meshs) {
mesh.material.color = Color.white;
}
if (isBossAtk)
rigid.velocity = Vector3.zero;
}
void OnDie() {
anim.SetTrigger("doDie");
isDead = true;
manager.GameOver();
}
void OnTriggerStay(Collider other) {
if (other.tag == "Weapon" || other.tag == "Shop")
nearObject = other.gameObject;
}
void OnTriggerExit(Collider other) {
if (other.tag == "Weapon")
nearObject = null;
else if (other.tag == "Shop") {
Shop shop = nearObject.GetComponent<Shop>();
shop.Exit();
isShop = false;
nearObject = null;
}
}
}
//GameManager 스크립트 파일
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
public class GameManager : MonoBehaviour {
public GameObject menuCam;
public GameObject gameCam;
public Player player;
public Boss boss;
public GameObject itemShop;
public GameObject weaponShop;
public GameObject startZone;
public int stage;
public float playTime;
public bool isBattle;
public int enemyCntA;
public int enemyCntB;
public int enemyCntC;
public int enemyCntD;
public Transform[] enemyZones;
public GameObject[] enemies;
public List<int> enemyList;
public GameObject menuPanel;
public GameObject gamePanel;
public GameObject overPanel;
public Text maxScoreTxt;
public Text scoreTxt;
public Text stageTxt;
public Text playTimeTxt;
public Text playerHealthTxt;
public Text playerAmmoTxt;
public Text playerCoinTxt;
public Image weapon1Img;
public Image weapon2Img;
public Image weapon3Img;
public Image weaponRImg;
public Text enemyATxt;
public Text enemyBTxt;
public Text enemyCTxt;
public RectTransform bossHealthGroup;
public RectTransform bossHealthBar;
public Text curScoreText;
public Text bestText;
void Awake() {
enemyList = new List<int>();
maxScoreTxt.text = string.Format("{0:n0}", PlayerPrefs.GetInt("MaxScore")); //세자리 수마다 , 추가
}
public void GameStart() {
menuCam.SetActive(false);
gameCam.SetActive(true);
menuPanel.SetActive(false);
gamePanel.SetActive(true);
player.gameObject.SetActive(true);
}
public void GameOver() {
gamePanel.SetActive(false);
overPanel.SetActive(true);
curScoreText.text = scoreTxt.text;
int maxScore = PlayerPrefs.GetInt("MaxsScore");
if (player.score > maxScore) {
bestText.gameObject.SetActive(true);
PlayerPrefs.SetInt("MaxsScore", player.score);
}
}
public void Restart() {
SceneManager.LoadScene(0);
}
public void StageStart() {
itemShop.SetActive(false);
weaponShop.SetActive(false);
startZone.SetActive(false);
foreach (Transform zone in enemyZones)
zone.gameObject.SetActive(true);
isBattle = true;
StartCoroutine(InBattle());
}
public void StageEnd() {
player.transform.position = Vector3.up * 0.8f;
itemShop.SetActive(true);
weaponShop.SetActive(true);
startZone.SetActive(true);
foreach (Transform zone in enemyZones)
zone.gameObject.SetActive(false);
isBattle = false;
stage++;
}
IEnumerator InBattle() {
if (stage % 5 == 0) {
enemyCntD++;
GameObject instantEnemy = Instantiate(enemies[3], enemyZones[0].position, enemyZones[0].rotation);
Enemy enemy = instantEnemy.GetComponent<Enemy>();
enemy.target = player.transform;
enemy.manager = this; //GameManager 보내주기
boss = instantEnemy.GetComponent<Boss>();
}
else {
for (int index = 0; index < stage; index++) {
int ran = Random.Range(0, 3);
enemyList.Add(ran);
switch (ran) {
case 0:
enemyCntA++;
break;
case 1:
enemyCntB++;
break;
case 2:
enemyCntC++;
break;
}
}
while (enemyList.Count > 0) {
int ranZone = Random.Range(0, 4);
GameObject instantEnemy = Instantiate(enemies[enemyList[0]], enemyZones[ranZone].position, enemyZones[ranZone].rotation);
Enemy enemy = instantEnemy.GetComponent<Enemy>();
enemy.target = player.transform;
enemy.manager = this; //GameManager 보내주기
enemyList.RemoveAt(0);
yield return new WaitForSeconds(4f);
}
}
while(enemyCntA + enemyCntB + enemyCntC + enemyCntD > 0) {
yield return null;
}
yield return new WaitForSeconds(4f);
boss = null;
StageEnd();
}
void Update() {
if (isBattle)
playTime += Time.deltaTime;
}
void LateUpdate() {
//좌측 상단 UI
scoreTxt.text = string.Format("{0:n0}", player.score);
//우측 상단 UI
stageTxt.text = "STAGE " + stage;
int hour = (int)(playTime / 3600); //시
int min = (int)((playTime - hour * 3600) / 60); //분
int second = (int)(playTime % 60); //초
playTimeTxt.text = string.Format("{0:00}", hour) + ":" + string.Format("{0:00}", min) + ":" + string.Format("{0:00}", second);
//플레이어(좌측 하단) UI
playerHealthTxt.text = player.health + " / " + player.maxHealth;
playerCoinTxt.text = string.Format("{0:n0}", player.coin);
if (player.equipWeapon == null)
playerAmmoTxt.text = "- / " + player.ammo;
else if (player.equipWeapon.type == Weapon.Type.Melee)
playerAmmoTxt.text = "- / " + player.ammo;
else
playerAmmoTxt.text = player.equipWeapon.curAmmo + " / " + player.ammo;
//무기(중앙 하단) UI
weapon1Img.color = new Color(1, 1, 1, (player.hasWeapons[0] ? 1 : 0));
weapon2Img.color = new Color(1, 1, 1, (player.hasWeapons[1] ? 1 : 0));
weapon3Img.color = new Color(1, 1, 1, (player.hasWeapons[2] ? 1 : 0));
weaponRImg.color = new Color(1, 1, 1, (player.hasGrenades > 0 ? 1 : 0));
//몬스터 숫자(우측 하단) UI
enemyATxt.text = enemyCntA.ToString();
enemyBTxt.text = enemyCntB.ToString();
enemyCTxt.text = enemyCntC.ToString();
//보스 체력(중앙 상단) UI
if (boss != null) {
bossHealthGroup.anchoredPosition = Vector3.down * 30;
bossHealthBar.localScale = new Vector3((float)boss.curHealth / boss.maxHealth, 1, 1);
}
else {
bossHealthGroup.anchoredPosition = Vector3.up * 200;
}
}
}
+) 버튼이 키보드 입력으로 클릭되는 경우 방지
5. 빌드
//GameManager 스크립트 파일
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
public class GameManager : MonoBehaviour {
public GameObject menuCam;
public GameObject gameCam;
public Player player;
public Boss boss;
public GameObject itemShop;
public GameObject weaponShop;
public GameObject startZone;
public int stage;
public float playTime;
public bool isBattle;
public int enemyCntA;
public int enemyCntB;
public int enemyCntC;
public int enemyCntD;
public Transform[] enemyZones;
public GameObject[] enemies;
public List<int> enemyList;
public GameObject menuPanel;
public GameObject gamePanel;
public GameObject overPanel;
public Text maxScoreTxt;
public Text scoreTxt;
public Text stageTxt;
public Text playTimeTxt;
public Text playerHealthTxt;
public Text playerAmmoTxt;
public Text playerCoinTxt;
public Image weapon1Img;
public Image weapon2Img;
public Image weapon3Img;
public Image weaponRImg;
public Text enemyATxt;
public Text enemyBTxt;
public Text enemyCTxt;
public RectTransform bossHealthGroup;
public RectTransform bossHealthBar;
public Text curScoreText;
public Text bestText;
void Awake() {
enemyList = new List<int>();
maxScoreTxt.text = string.Format("{0:n0}", PlayerPrefs.GetInt("MaxScore")); //세자리 수마다 , 추가
if (PlayerPrefs.HasKey("MaxScore"))
PlayerPrefs.SetInt("MaxScore", 0);
}
public void GameStart() {
menuCam.SetActive(false);
gameCam.SetActive(true);
menuPanel.SetActive(false);
gamePanel.SetActive(true);
player.gameObject.SetActive(true);
}
public void GameOver() {
gamePanel.SetActive(false);
overPanel.SetActive(true);
curScoreText.text = scoreTxt.text;
int maxScore = PlayerPrefs.GetInt("MaxsScore");
if (player.score > maxScore) {
bestText.gameObject.SetActive(true);
PlayerPrefs.SetInt("MaxsScore", player.score);
}
}
public void Restart() {
SceneManager.LoadScene(0);
}
public void StageStart() {
itemShop.SetActive(false);
weaponShop.SetActive(false);
startZone.SetActive(false);
foreach (Transform zone in enemyZones)
zone.gameObject.SetActive(true);
isBattle = true;
StartCoroutine(InBattle());
}
public void StageEnd() {
player.transform.position = Vector3.up * 0.8f;
itemShop.SetActive(true);
weaponShop.SetActive(true);
startZone.SetActive(true);
foreach (Transform zone in enemyZones)
zone.gameObject.SetActive(false);
isBattle = false;
stage++;
}
IEnumerator InBattle() {
if (stage % 5 == 0) {
enemyCntD++;
GameObject instantEnemy = Instantiate(enemies[3], enemyZones[0].position, enemyZones[0].rotation);
Enemy enemy = instantEnemy.GetComponent<Enemy>();
enemy.target = player.transform;
enemy.manager = this; //GameManager 보내주기
boss = instantEnemy.GetComponent<Boss>();
}
else {
for (int index = 0; index < stage; index++) {
int ran = Random.Range(0, 3);
enemyList.Add(ran);
switch (ran) {
case 0:
enemyCntA++;
break;
case 1:
enemyCntB++;
break;
case 2:
enemyCntC++;
break;
}
}
while (enemyList.Count > 0) {
int ranZone = Random.Range(0, 4);
GameObject instantEnemy = Instantiate(enemies[enemyList[0]], enemyZones[ranZone].position, enemyZones[ranZone].rotation);
Enemy enemy = instantEnemy.GetComponent<Enemy>();
enemy.target = player.transform;
enemy.manager = this; //GameManager 보내주기
enemyList.RemoveAt(0);
yield return new WaitForSeconds(4f);
}
}
while(enemyCntA + enemyCntB + enemyCntC + enemyCntD > 0) {
yield return null;
}
yield return new WaitForSeconds(4f);
boss = null;
StageEnd();
}
void Update() {
if (isBattle)
playTime += Time.deltaTime;
}
void LateUpdate() {
//좌측 상단 UI
scoreTxt.text = string.Format("{0:n0}", player.score);
//우측 상단 UI
stageTxt.text = "STAGE " + stage;
int hour = (int)(playTime / 3600); //시
int min = (int)((playTime - hour * 3600) / 60); //분
int second = (int)(playTime % 60); //초
playTimeTxt.text = string.Format("{0:00}", hour) + ":" + string.Format("{0:00}", min) + ":" + string.Format("{0:00}", second);
//플레이어(좌측 하단) UI
playerHealthTxt.text = player.health + " / " + player.maxHealth;
playerCoinTxt.text = string.Format("{0:n0}", player.coin);
if (player.equipWeapon == null)
playerAmmoTxt.text = "- / " + player.ammo;
else if (player.equipWeapon.type == Weapon.Type.Melee)
playerAmmoTxt.text = "- / " + player.ammo;
else
playerAmmoTxt.text = player.equipWeapon.curAmmo + " / " + player.ammo;
//무기(중앙 하단) UI
weapon1Img.color = new Color(1, 1, 1, (player.hasWeapons[0] ? 1 : 0));
weapon2Img.color = new Color(1, 1, 1, (player.hasWeapons[1] ? 1 : 0));
weapon3Img.color = new Color(1, 1, 1, (player.hasWeapons[2] ? 1 : 0));
weaponRImg.color = new Color(1, 1, 1, (player.hasGrenades > 0 ? 1 : 0));
//몬스터 숫자(우측 하단) UI
enemyATxt.text = enemyCntA.ToString();
enemyBTxt.text = enemyCntB.ToString();
enemyCTxt.text = enemyCntC.ToString();
//보스 체력(중앙 상단) UI
if (boss != null) {
bossHealthGroup.anchoredPosition = Vector3.down * 30;
bossHealthBar.localScale = new Vector3((float)boss.curHealth / boss.maxHealth, 1, 1);
}
else {
bossHealthGroup.anchoredPosition = Vector3.up * 200;
}
}
}
- 첫 실행시 최고 점수가 기록되어 있지 않을 것이므로, 0으로 초기화해주는 작업을 Awake에 추가해주었다.
'유니티 프로젝트 > 3D 쿼터뷰 액션게임' 카테고리의 다른 글
[Unity/유니티] 기초-3D 쿼터뷰 액션 게임: UI 로직 연결하기[B53] (0) | 2022.04.10 |
---|---|
[Unity/유니티] 기초-3D 쿼터뷰 액션 게임: 간단한 상점 만들기[B52] (0) | 2022.04.09 |
[Unity/유니티] 기초-3D 쿼터뷰 액션 게임: UI 배치하기 [B51] (0) | 2022.04.08 |
[Unity/유니티] 기초-3D 쿼터뷰 액션 게임: 다양한 패턴을 구사하는 보스 만들기[B50] (0) | 2022.04.07 |
[Unity/유니티] 기초-3D 쿼터뷰 액션 게임: 다양한 몬스터 만들기[B49] (0) | 2022.04.03 |