2023. 12. 13. 20:55ㆍUnity
https://create.unity.com/performance-optimization-e-book-console-PC?ungated=true
위의 링크에 들어가면 게임 성능 최적화에 대한 E-Book을 다운로드 받을 수 있다. 물론 게임뿐만 아니라 유니티 프로젝트 자체의 최적화에 두루 쓸 수 있는 내용이다.
프로젝트 최적화
Profiling, Memory, Assets, Graphics, GPU optimiztion 등에 대한 최적화 기능들이 책에 잘 나와있다.
현재는 가만히 있을 때 17 FPS, 화면을 움직일 때는 1~4 FPS까지 떨어진다.(다행히 Build를 하면 조금 낫다.)
[Window] - [Analysis] - [Frame Debugger]를 열어서 가장 많은 에셋들이 보일 때 Enable을 눌러보면, 현재 씬에서는 9.9백만 개의 Triangle이 보여지고 있고, 4146개의 Batches 수를 볼 수 있다. 이 상태에서의 DrawCall은 4171이 뜨는 것을 볼 수 있다.
드로우 콜을 줄이기 위해서 세부 항목을 열어보면, [Render.OpaqueGeometry] - [RenderDeferred.GBuffer]에서 Static Batch, Draw Mesh 수가 엄청 많은 것을 볼 수 있다.
[RenderDeferred.Lighting] 항목도 많은 드로우콜 수를 가지고 있는 것을 볼 수 있다. Realtime 라이트를 쓰는 것도 드로우콜 증가에 큰 몫을 한 것 같다.
Static Batching과 나뭇잎과 같은 중복 에셋들에 대한 최적화를 진행해야 될 것 같다. 라고 생각했지만 SRP Batcher를 사용하기 위해서는 URP 프로젝트로 만들어졌어야 했다.... 지금 프로젝트는 Built-In이다.
GPU Instancing은 사용이 가능할 것 같다.
재질 Inspector 창에 모든 메테리얼의 [Enable GPU Instancing]을 체크 한다.
중간에 Reflection Probe가 Realtime으로 설정 되어있어서 필요없는 건물 내부의 것들은 비활성화 시켰다.
이전과 비교하면 나름 많이 나아졌다!
최대한 같은 화면을 기준으로, 개선점은 다음과 같다.
프레임 수: 9.4fps -> 24.7fps
Batches: 4904 -> 2314
Tris: 11.0M -> 4.5M
Verts: 15.8M -> 6.3M
모델링 교체
적군 비행기 모델도 Triangles 수가 29432개라서 상당히 높아 보이긴 해서 낮은 모델로 바꾸면 좋을 것 같다.
폴리 수가 훨씬 적은 다른 기종으로 적의 모델을 바꾼다.
https://assetstore.unity.com/packages/3d/vehicles/air/super-spitfire-53217
스크립트에서 Hierarchy 상에서 데미지를 입을 때 이름에서 "Enemy"가 포함된 오브젝트를 받아오도록 해서 이름을 Enemy로 바꿔줘도 반응을 하지 않는다. 아마 이 프리팹의 트리 구조 때문인 것으로 보인다.
이 부분을 Enemy Tag를 인식 하는 것으로 바꿔본다.
PlayerFire 스크립트에서,
/*
//Ray 충돌 정보가 Enemy 이름을 포함하고 있다면, (프리팹은 뒤에 괄호로 숫자도 들어감)
if(hitInfo.transform.name.Contains("Enemy"))
{
EnemyAI enemy = hitInfo.transform.GetComponent<EnemyAI>(); //EnemyAI 스크립트 가져오기
if (enemy) //만약 enemy가 들어왔다면,
{
enemy.DamageProcess(); //enemy 스크립트의 DamageProcess 함수를 불러오기
}
}
*/
//Ray 충돌 정보가 Enemy 태그를 포함하고 있다면,
if (hitInfo.transform.gameObject.CompareTag("Enemy"))
{
EnemyAI enemy = hitInfo.transform.gameObject.GetComponent<EnemyAI>(); //EnemyAI 스크립트 가져오기
if (enemy) //만약 enemy가 들어왔다면,
{
Debug.Log("적에게 발사");
enemy.DamageProcess(); //enemy 스크립트의 DamageProcess 함수를 불러오기
}
}
태그를 쓰는 것으로 바꿨지만 되지 않았다. Debug.Log("적에게 발사") 로그가 뜨지 않는다. 근데 과거에 쓰던 비행기 모델에 태그를 썼더니 적용이 되었다. 결국 날개, 동체, 프로펠러 등 모델이 다 나눠져 있던 것이 Ray가 맞지 않은 문제라고 생각된다.
블렌더를 이용해서 Mesh를 하나로 합친다. 원래는 많은 메시가 있었는데 Ctrl + J로 합쳐준다.
유니티에서 프로펠러쪽이 땅에 박혀서 다니는 형상이어서 아예 블렌더에서 Rotation을 적용했다.
이 상태로 유니티에 적용하니 온전한 모습으로 맵을 돌아다니게 되었다. (나름 오래 걸림 ㅠㅠ)
다행히 Coroutine으로 잠시 멈춤도 적용되고, 비행기 폭발 효과도 잘 적용 되었다.
로비 씬 넣기
로비 씬 UI를 설정한다. 이전에 FPS 게임에서 썼던 로비씬 이미지와 같은데 뒷 배경이 이번 타워디펜스 게임에도 잘 어울릴 것 같아서 그대로 적용해본다.
BATTLE 버튼 부분은 알파 값을 높여 투명 처리하려 했는데 그러면 누르는 느낌이 들지가 않아서 해당 부분 스크린샷 스프라이트 이미지를 적용했다. 추가로 버튼 부분 좀 더 밝게하여 하이라이트 처리 하였다.
로비 씬도 Build Settings에 넣고 메인 씬 인덱스를 확인한 후 SceneChange 스크립트를 생성한다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class SceneChange : MonoBehaviour
{
public void LoadScene()
{
//빌드 세팅 씬 넘버로 씬 불러오기
SceneManager.LoadScene(1);
}
}
조금의 로딩 시간이 필요하지만 씬 전환이 잘 된다.
Pause, Continue, Restart, Quit UI 설정
로비 씬에서 메인 씬으로 들어갈 때 게임이 바로 시작되면 마우스 움직임이 조금 불편해지기 때문에 메인 씬으로 전환 후 3초 정도 딜레이 타임을 넣고 이후에 게임이 시작되게 만들어야 할 것 같다.
추가로, 인 게임 화면에서 일시정지와 재시작, 나가기 버튼도 만들어서 적용한다.
UI를 먼저 만든다.
메뉴 폰트를 위해서 구글 폰트를 다운로드한다. 메뉴 이미지를 위해 검색해서 알맞은 이미지를 골라서 사용하였다.
ButtonMananger 스크립트를 만들어서 아래의 코드를 사용할 수 있다. 메뉴 버튼을 눌렀을 때 Restart와 Quit에 대응하는 함수를 만들었다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class ButtonManager : MonoBehaviour
{
public void RestartGame()
{
Time.timeScale = 1.0f;
//현재 씬 리로드
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
}
public void QuitGame()
{
Application.Quit();
}
}
GameManager에서는 옵션 패널 자체를 켰다 끄는 코드를 추가하였다.
public void OpenOption() //메뉴 옵션을 켜는 함수
{
gameOption.SetActive(true);
Time.timeScale = 0;
//gameState = GameState.Pause;
}
public void CloseOption() //Continue를 눌렀을 때 메뉴 옵션을 끄는 함수
{
gameOption.SetActive(false);
Time.timeScale = 1.0f;
//gameState = GameState.Go;
}
GameManager 전체 코드
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameManager : MonoBehaviour
{
//적 오브젝트 가져오기
public GameObject enemy;
//적 생성 딜레이 시간
float currentTime = 0;
float delayTime = 3f;
//UI 패널 가져오기
public GameObject gameOption;
void Update()
{
currentTime += Time.deltaTime;
if(currentTime > delayTime)
{
//EnemyPoints 그룹의 자식들 위치 중 랜덤한 위치에 생성. 배열 사용. GetComponents를 사용해야함.
Transform[] points = GameObject.Find("EnemyPoints").GetComponentsInChildren<Transform>();
//랜덤 위치 시작은 1, (부모가 0) 끝은 포인트의 길이
int idx = Random.Range(1, points.Length);
//적 생성 위치와 회전도 같이 가져온다.
Instantiate(enemy, points[idx].position, points[idx].rotation);
currentTime = 0;
}
}
public void OpenOption()
{
gameOption.SetActive(true);
Time.timeScale = 0;
//gameState = GameState.Pause; //나중에 코루틴 생성하면 쓸 예정
}
public void CloseOption()
{
gameOption.SetActive(false);
Time.timeScale = 1.0f;
//gameState = GameState.Go; //나중에 코루틴 생성하면 쓸 예정
}
}
메뉴 옵션이 잘 적용되었다.
Gameover UI 만들기
이전에 MenuPanel을 복사해서 UI Panel을 하나 만든다. 여기서 Continue 버튼은 지운다. 대신 그 자리에 GameOver 텍스트를 넣는다.
UI는 만들었고, 플레이어의 체력이 0일 때 패널이 SetActive(true);가 되어야한다.
적의 공격력이 없었으므로 EnemyAI 스크립트에서 변수 선언을 한다.
//적 공격력 선언
public int attackPower = 10;
//플레이어 가져오기
GameObject player;
Start 함수에서 "Player"라는 이름으로 오브젝트를 가져온다.
//플레이어 게임오브젝트 선언
player = GameObject.Find("Player").gameObject;
그리고 코드의 밑으로 내려가서 Attack() 함수에서 플레이어 스크립트를 가져와서 DamageAction 함수를 실행하는 코드를 쓴다.
void Attack()
{
currentTime += Time.deltaTime;
if(currentTime > attackDelayTime)
{
Debug.Log("공격!");
player.GetComponent<PlayerMove>().DamageAction(attackPower);
currentTime = 0;
}
}
현재까지 EnemyAI 전체 스크립트
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class EnemyAI : MonoBehaviour
{
//상태 상수 정의
enum EnemyState
{
Idle, Move, Attack, Damage, Die
}
//초기 시작 상태 설정
EnemyState state = EnemyState.Idle;
//대기 지속 시간
public float idleDelayTime = 2f;
//경과 시간
float currentTime = 0;
//이동 속도
public float moveSpeed = 5f;
//타워 위치
Transform tower;
//네비게이션 메시 에이전트 가져오기
NavMeshAgent agent;
//공격 범위
public float attackRange = 30f;
//공격 지연 시간
public float attackDelayTime = 2f;
//적 체력
public int enemyHp = 3;
//폭발 효과
Transform explosion;
//폭발 파티클 시스템 변수 선언
ParticleSystem explosionEffect;
//적 공격력 선언
public int attackPower = 10;
//플레이어 가져오기
GameObject player;
void Start()
{
//타워를 이름으로 가져오기
tower = GameObject.Find("TowerObject").transform;
agent = GetComponent<NavMeshAgent>();
//처음 상태에서는 비활성화 되어야한다.
agent.enabled = false;
agent.speed = moveSpeed;
//폭발 프리팹을 이름으로 가져오기
explosion = GameObject.Find("Explosion").transform;
//파티클 시스템 가져오기
explosionEffect = explosion.GetComponent<ParticleSystem>();
//플레이어 게임오브젝트 선언
player = GameObject.Find("Player").gameObject;
}
void Update()
{
switch (state)
{
case EnemyState.Idle:
Idle();
break;
case EnemyState.Move:
Movement();
break;
case EnemyState.Attack:
Attack();
break;
}
}
void Idle()
{
//시간의 경과
currentTime += Time.deltaTime;
if(currentTime > idleDelayTime)
{
//일정 시간 이상 지나면 이동 상태로 전환
state = EnemyState.Move;
print("이동 상태");
//에이전트 활성화
agent.enabled = true;
}
}
void Movement()
{
agent.SetDestination(tower.position);
//공격 범위 안에 들어오면 공격 상태로 전환
if(Vector3.Distance(transform.position, tower.position) < attackRange)
{
state = EnemyState.Attack;
//공격 상태일 때는 NavMesh 비활성화
agent.enabled = false;
}
}
void Attack()
{
currentTime += Time.deltaTime;
if(currentTime > attackDelayTime)
{
Debug.Log("공격!");
player.GetComponent<PlayerMove>().DamageAction(attackPower);
currentTime = 0;
}
}
public void DamageProcess()
{
//데미지를 맞으면 적의 체력이 깎임 PlayerFire 스크립트에서 접근 가능하도록 public 함수로 생성
enemyHp--;
Debug.Log("적 체력: " + enemyHp);
if(enemyHp > 0) //적의 체력이 양수라면,
{
state = EnemyState.Damage;
//밑에서 만든 코루틴 호출
StopAllCoroutines(); //현재 재생 중인 모든 Coroutine 중지 초기화 느낌
StartCoroutine(Damage());
}
else //적 체력이 0 이하일 때
{
//폭발 효과를 적의 현재 위치로 가져오기
explosion.position = transform.position;
Debug.Log(explosion.position);
Debug.Log(transform.position);
explosionEffect.Play();
//적 제거
Destroy(gameObject);
}
}
//코루틴 생성
IEnumerator Damage()
{
//피격되면 잠깐 Idle 상태로 전환 했다가 다시 원래대로 전환
agent.enabled = false; //길찾기 중지
yield return new WaitForSeconds(0.1f);
state = EnemyState.Idle;
currentTime = 0; //맞았으니까 시간 초기화
}
}
PlayerMove 스크립트에서도 코드를 추가한다.
플레이어 체력 추가 (20번 정도 맞으면 GameOver)
//플레이어 체력
public int playerHP = 200;
Canvas는 public으로 직접 가져오고, GameOver 패널은 가져온 캔버스의 자식오브젝트로 찾을 것이다.
//캔버스 가져오기
public GameObject canvas;
//게임오버 패널
GameObject gameOverPanel;
void Start()
{
//캐릭터 컨트롤러 가져오기
cc = GetComponent<CharacterController>();
//게임오버 패널을 자식 오브젝트 인덱스로 찾기
gameOverPanel = canvas.transform.GetChild(2).gameObject;
}
DamageAction 함수를 추가했다. GameOver 패널 뜨면서 게임 멈춤.
public void DamageAction(int damage)
{
playerHP -= damage; //플레이어 체력 감소
if (playerHP < 0)
{
playerHP = 0;
gameOverPanel.SetActive(true);
Time.timeScale = 0;
}
Debug.Log("타워 체력: " + playerHP);
}
PlayerMove 전체 코드
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//XRInput 스크립트를 사용해서 플레이어 움직이기
public class PlayerMove : MonoBehaviour
{
//이동 속도
public float speed = 10f;
//캐릭터 컨트롤러 컴포넌트를 변수로 가져오기
CharacterController cc;
//중력 변수
public float gravity = -20f;
//수직 속도 초기화
float yVelocity = 0;
//점프 파워
public float jumpPower = 7f;
//플레이어 체력
public int playerHP = 20;
//캔버스 가져오기
public GameObject canvas;
//게임오버 패널
GameObject gameOverPanel;
void Start()
{
//캐릭터 컨트롤러 가져오기
cc = GetComponent<CharacterController>();
//게임오버 패널을 자식 오브젝트 인덱스로 찾기
gameOverPanel = canvas.transform.GetChild(2).gameObject;
}
void Update()
{
float horizontal = XRInput.GetAxis("Horizontal");
float vertical = XRInput.GetAxis("Vertical");
//방향 설정
Vector3 dir = new Vector3(horizontal, 0, vertical);
//카메라가 바라보는 방향으로 가라
dir = Camera.main.transform.TransformDirection(dir);
//중력 적용
yVelocity += gravity * Time.deltaTime;
//점프 버튼을 누르면 점프 XRInput.cs의 191번째줄 참고
if(XRInput.GetDown(XRInput.Button.Two, XRInput.Controller.RTouch))
{
//점프 파워 적용
yVelocity = jumpPower;
}
dir.y = yVelocity;
//이동
cc.Move(dir * speed * Time.deltaTime);
}
public void DamageAction(int damage)
{
playerHP -= damage; //플레이어 체력 감소
if (playerHP < 0)
{
playerHP = 0;
gameOverPanel.SetActive(true);
Time.timeScale = 0;
}
Debug.Log("타워 체력: " + playerHP);
}
}
체력이 0 미만으로 떨어지면 GameOver 패널이 팝업 된다.
플레이어 점프 횟수 제한
지금은 플레이어가 무제한 점프를 할 수 있기 때문에 다소 어색할 수 있고 하늘까지 날아갈 수도 있다. 점프를 최대 2회까지만 하도록 제한한다.
PlayerMove 스크립트를 수정한다. isJumping 멤버 변수 추가
//점프 상태 변수, 처음엔 점프 안하므로 false
public int isJumping = 0;
바닥에 닿으면 yVelocity 초기화, isJumping 초기화
//캐릭터가 바닥에 착지 했다면 = 바닥면이 닿았다면
if (cc.collisionFlags == CollisionFlags.Below)
{
isJumping = 0;
yVelocity = 0; //값을 0으로 초기화
}
점프 누를 때 isJumping 값이 한 번도 뛰지 않았거나 한 번 뛰어서 1이라면 뛴다. 하지만 두 번 뛴 2값이 들어오면 더 뛰지 못한다.
//점프 버튼을 누르면 점프 XRInput.cs의 191번째줄 참고
if (XRInput.GetDown(XRInput.Button.Two, XRInput.Controller.RTouch))
{
if(isJumping == 0 || isJumping == 1)
{
//점프 파워 적용
yVelocity = jumpPower;
isJumping += 1; //한 번 점프할 때마다 1씩 증가
}
}
최대 두 번까지 점프되는 것이 잘 적용되었다.
Firebase로 로그인 씬 만들기
UI 설정
[Project Settings] - [Player]에서 CompanyName과 Product Name을 설정한다.
Firebase에서, Unity에서 설정했던 [Bundle Identifier] 이름으로 생성한다.
json을 받아 [Assets] - [StreamingAssets] 폴더에 json을 추가한다.
FrebaseAuth.unitypackage SDK를 import 한다. [Assets] - [Import Package] - [Custom Package...]
두 오류가 뜨는데 IOS 설치를 하지 않았기 때문이다.
다음 경로의 파일에서 체크박스 모두 해제하고 Apply.
다음 오류는 UNITY_IOS를 입력하고 Apply해서 해결한다.
FirebaseAuthManager 스크립트를 생성한다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Firebase.Auth;
using TMPro;
public class FirebaseAuthManager : MonoBehaviour
{
private FirebaseAuth auth; //인증을 위한 변수 선언
public TMP_InputField email;
public TMP_InputField password;
void Start()
{
auth = FirebaseAuth.DefaultInstance;
}
public void Create() //계정 생성
{
auth.CreateUserWithEmailAndPasswordAsync(email.text, password.text).ContinueWith(task => //람다식, 결과는 task가 나온다.
{
if (task.IsFaulted) //계정을 만들지 못했을 경우,
{
Debug.Log("계정 생성 실패");
return;
}
if (task.IsCanceled) //계정 생성을 실패했을 경우(네트워크 장애, 도중 취소)
{
Debug.Log("계정 생성 취소");
return;
}
FirebaseUser newUser = task.Result.User; //계정을 만들지 못하거나 실패했을 경우가 아닐 경우
});
}
public void LogIn() //로그인 함수 계정 생성과 동일한 코드 복붙, 수정
{
auth.SignInWithEmailAndPasswordAsync(email.text, password.text).ContinueWith(task => //람다식, 결과는 task가 나온다.
{
if (task.IsFaulted) //로그인을 못했을 경우,
{
Debug.Log("로그인 실패");
return;
}
if (task.IsCanceled) //로그인을 실패했을 경우(네트워크 장애, 도중 취소)
{
Debug.Log("로그인 취소");
return;
}
Debug.Log("로그인 성공");
});
}
public void LogOut() //로그아웃 함수
{
auth.SignOut();
Debug.Log("로그아웃"); //로그아웃 확인
}
}
각 InputField에 맞는 값을 넣는다.
씬 전환을 위해 Build Settings에 Login 씬 추가
바로 위의 코드에서 로그인 성공하면 다음 씬으로 전환하는 코드를 넣는다.
public void LogIn() //로그인 함수 계정 생성과 동일한 코드 복붙, 수정
{
auth.SignInWithEmailAndPasswordAsync(email.text, password.text).ContinueWith(task => //람다식, 결과는 task가 나온다.
{
if (task.IsFaulted) //로그인을 못했을 경우,
{
Debug.Log("로그인 실패");
return;
}
if (task.IsCanceled) //로그인을 실패했을 경우(네트워크 장애, 도중 취소)
{
Debug.Log("로그인 취소");
return;
}
Debug.Log("로그인 성공");
SceneManager.LoadScene(1);
});
}
버튼에 함수 매핑
실행하고 아이디와 비밀번호를 Create하면, Firebase에 생성이 된다.
그런데 로그인은 성공했지만 씬 전환이 되지 않아서 Coroutine을 이용해서 로그인 후 씬 전환에 딜레이 타임을 넣었다.
씬 전환이 되지 않은 이유는 Firebase의 SignInWithEmailAndPasswordAsync 함수는 비동기 함수이지만, 현재 코드에서는 즉시 다음 코드를 실행하고 있기 때문이었다. 코드를 다음과 같이 수정하였다.
public async void LogIn() //로그인 함수 계정 생성과 동일한 코드 복붙, 수정
{
try
{
var signInTask = await auth.SignInWithEmailAndPasswordAsync(email.text, password.text);
Debug.Log("로그인 성공");
SceneManager.LoadScene(1);
}
catch (System.Exception e)
{
Debug.Log($"로그인 실패: {e.Message}");
}
}
로그인 실패시, Text로 실패했다고 표시도 해준다.
텍스트를 가운데에 하나 만들고,
텍스트를 public으로 받아오고,
public TMP_Text loginFailedtext;
로그인 성공했을 때와 실패했을 때 각각 텍스트에 메시지를 출력한다.
public async void LogIn() //로그인 함수 계정 생성과 동일한 코드 복붙, 수정
{
try
{
var signInTask = await auth.SignInWithEmailAndPasswordAsync(email.text, password.text);
Debug.Log("로그인 성공");
loginFailedtext.text = "Login Success!";
SceneManager.LoadScene(1);
}
catch (System.Exception e)
{
Debug.Log($"로그인 실패: {e.Message}");
loginFailedtext.text = "Login Failed";
}
}
FirebaseAuthManager 전체 코드
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Firebase.Auth;
using TMPro;
using UnityEngine.SceneManagement;
public class FirebaseAuthManager : MonoBehaviour
{
private FirebaseAuth auth; //인증을 위한 변수 선언
public TMP_InputField email;
public TMP_InputField password;
public TMP_Text loginFailedtext;
void Start()
{
auth = FirebaseAuth.DefaultInstance;
}
public void Create() //계정 생성
{
auth.CreateUserWithEmailAndPasswordAsync(email.text, password.text).ContinueWith(task => //람다식, 결과는 task가 나온다.
{
if (task.IsFaulted) //계정을 만들지 못했을 경우,
{
Debug.Log("계정 생성 실패");
return;
}
if (task.IsCanceled) //계정 생성을 실패했을 경우(네트워크 장애, 도중 취소)
{
Debug.Log("계정 생성 취소");
return;
}
FirebaseUser newUser = task.Result.User; //계정을 만들지 못하거나 실패했을 경우가 아닐 경우
});
}
public async void LogIn() //로그인 함수 계정 생성과 동일한 코드 복붙, 수정
{
try
{
var signInTask = await auth.SignInWithEmailAndPasswordAsync(email.text, password.text);
Debug.Log("로그인 성공");
loginFailedtext.text = "Login Success!";
SceneManager.LoadScene(1);
}
catch (System.Exception e)
{
Debug.Log($"로그인 실패: {e.Message}");
loginFailedtext.text = "Login Failed";
}
}
public void LogOut() //로그아웃 함수
{
auth.SignOut();
Debug.Log("로그아웃"); //로그아웃 확인
}
}
로그인 실패해서 Login Failed 텍스트 출력, 이후 성공해서 Lobby 씬으로 넘어간 후 Battle 버튼을 눌러 게임을 시작한다.
더 넣고 싶은 기능들...
적 HP 표시, 내 HP 표시
적 위치 표시 -미니맵
시작하고 ready, go!
'Unity' 카테고리의 다른 글
[Robot Arm Arduino Project] RAAP프로젝트 -프로젝트 준비2(3D 프린터 사용 신청, 부품 주문 리스트업) (0) | 2023.12.15 |
---|---|
[Robot Arm Arduino Project] RAAP프로젝트 -프로젝트 준비(3D 프린터 사용 신청) (0) | 2023.12.14 |
[XR 전문 인력 과정] 유니티 타워디펜스 만들기(4) -적 공격하기, 적 폭발, 적 생성 (0) | 2023.12.11 |
[Unity] 던전앤파이터 API 활용 -진행중 (3) | 2023.12.07 |
[XR 전문 인력 과정] 유니티 타워디펜스 만들기(3) -HDRI 적용, AINavMeshAgent 적용, 적이 타워를 공격하게 하기 (1) | 2023.11.29 |