2023. 7. 7. 00:59ㆍSWU_프로젝트/GURU - unity
오늘 학습할 내용은 아래와 같다.
1. 싱글톤패턴으로 관리자 만들기
2. 오브젝트풀을 이용한 메모리 관리(기초)
3. 오브젝트풀을 이용한 메모리 관리(고급)
1. 싱글톤패턴으로 관리자 만들기
싱글톤 객체 만들기
public class ScoreManager : MonoBehaviour
{ . . . (생략) . . .
// 싱글톤 객체
public static ScoreManager Instance = null;
// 싱글톤 객체에 값이 없으면 생성된 자기 자신을 할당
void Awake()
{
if(Instance == null)
{
Instance = this;
}
}
. . . (생략) . . .
}
- 출처가 같은 변수는 단 하나만 선언 할 수 있다”는 규칙을 이용하여 싱글톤 디자인패턴을 만들어 사용한다.
- 싱글톤 디자인패턴은 단 하나의 객체 인스턴스를 static 으로 등록해 놓고 사용을 하는 방식이다.
- ScoreManager 가 점수를 관리하는 유일한 매니저가 되기 때문에 싱글톤객체로 만들어 사용한다.
- public static ScoreManager Instance = null; 전체 오브젝트에서 고유한 값을 가지게 된다.
- 불필요하게 find과정을 거칠 필요가 없어지고 바로 부를수 다.
싱글톤 객체 참조로 변환
public class Enemy : MonoBehaviour
{
//1. 적이 다른 물체와 충돌 했으니까.
private void OnCollisionEnter(Collision collision)
{
// 에너미를 잡을 때마다 현재 점수 표시하고 싶다. ScoreManager.Instance.SetScore(ScoreManager.Instance.GetScore() + 1);
//2.폭발 효과 공장에서 폭발 효과를 하나 만들어야 한다.
GameObject explosion = Instantiate(explosionFactory);
//3.폭발 효과를 발생(위치) 시키고 싶다.
explosion.transform.position = transform.position;
Destroy(collision.gameObject);
Destroy(gameObject);
}
}
Enemy.cs 스크립트로 이동하여 ScoreManager 의 Instance 맴버에 접근 하는 것으로 바꾼다.
find를 쓰지않기때문에 간결해지고 퍼포먼스도 더 좋아진다.
get/set 선언
public class ScoreManager : MonoBehaviour
{ . . . (생략) . . .
public static ScoreManager Instance = null;
public int Score
{
get
{
return currentScore;
}
set
{
// to do
}
}
}
- 사용법은 변수의 편의성을 가져오고, 제작은 함수의 몸체를 갖도록 만든다.
- ScoreManager 에 Score 이름을 갖는 get/set 프로퍼티를 만든다.
get/set 구현
public class ScoreManager : MonoBehaviour
{
public int Score
{
. . . (생략) . . .
set
{
// 3.ScoreManager 클래스의 속성에 값을 할당 한다.
currentScore = value;
// 4.화면에 현재 점수 표시하기
currentScoreUI.text = "현재점수 : " + currentScore;
//목표: 최고 점수를 표시하고 싶다.
//1.현재 점수가 최고 점수 보다 크니까
//-> 만약 현재 점수가 최고 점수를 초과 하였다면”
if (currentScore > BestScore)
{
//2.최고 점수가 갱신 시킨다.
BestScore = currentScore;
//3.최고 점수 UI 에 표시
BestScoreUI.text = "최고점수 : " + BestScore;
// 목표 : 최고점수를 저장하고싶다.
PlayerPrefs.SetInt("Best Score", BestScore);
}
}
}
}
- Set 의 구현부는 SetScore 함수의 구현 내용을 복사 붙여넣기 해준다.
- GetScore 함수와 SetScore 함수는 제거하여 준다.
get/set 프로퍼티로 구현 변경
public class Enemy : MonoBehaviour
{
//1. 적이 다른 물체와 충돌 했으니까.
private void OnCollisionEnter(Collision collision)
{
// 에너미를 잡을 때마다 현재 점수 표시하고 싶다.
ScoreManager.Instance.Score++;
//2.폭발 효과 공장에서 폭발 효과를 하나 만들어야 한다.
GameObject explosion = Instantiate(explosionFactory);
//3.폭발 효과를 발생(위치) 시키고 싶다.
explosion.transform.position = transform.position;
Destroy(collision.gameObject);
Destroy(gameObject);
}
}
Enemy 스크립트로 이동하여 Score 변경 코드를 다음 코드와 같이 바꾼다.
2. 오브젝트풀을 이용한 메모리 관리 (기초)
목표 : 총알과 에너미를 오브젝트풀로 관리하고 싶다.
순서 : 1.총알 오브젝트풀 제작
2. 에너미 오브젝트풀 제작
필요속성 추가
public class PlayerFire : MonoBehaviour
{
public GameObject bulletFactory;
// 탄창에 넣을 총알 개수
public int poolSize = 10;
// 오브젝트풀 배열
GameObject[] bulletObjectPool;
}
PlayerFire 에 오브젝트 풀 크기 속성과, 오브젝트 풀을 위한 배열을 추가하여 준다.
구현 과정
목표 : 태어 날 때 오브젝트풀(탄창) 에 총알을 하나씩 생성하여 넣고 싶다.
순서 : 1. 태어 날 때
2. 탄창을 총알 담을 수 있는 크기로 만들어 준다.
3. 탄창에 넣을 총알 개수 만큼 반복하여
4 총알공장에서 총알 생성한다.
5. 총알을 오브젝트풀에 넣고싶다.
1.태어날 때
public class PlayerFire : MonoBehaviour
{
. . . (생략) . . .
// 태어 날 때 오브젝트풀(탄창) 에 총알을 하나씩 생성하여 넣고 싶다
// 1. 태어 날 때
void Start()
{
// 2. 탄창을 총알 담을 수 있는 크기로 만들어 준다.
// 3. 탄창에 넣을 총알 개수 만큼 반복하여
// 4. 총알공장에서 총알 생성한다.
// 5. 총알을 오브젝트풀에 넣고싶다.
}
- PlayerFire 객체가 태어날 때 탄창에 총알을 만들어 넣어 주도록 한다.
- 라이프사이클 함수 중 Start 에서 그 처리를 한다.
2. 탄창을 총알 담을 수 있는 크기로 만들어 준다.
ublic class PlayerFire : MonoBehaviour
{ . . . (생략) . . .
// 태어 날 때 오브젝트풀(탄창) 에 총알을 하나씩 생성하여 넣고 싶다
// 1. 태어 날 때
void Start()
{
// 2. 탄창을 총알 담을 수 있는 크기로 만들어 준다.
bulletObjectPool = new GameObject[poolSize];
// 3. 탄창에 넣을 총알 개수 만큼 반복하여
// 4. 총알공장에서 총알 생성한다.
// 5. 총알을 오브젝트풀에 넣고싶다.
}
오브젝트 풀인 bulletObjectPool 배열을 풀사이즈 크기 만큼으로 생성한다.
3.탄창에 넣을 총알 개수 만큼 반복하여
public class PlayerFire : MonoBehaviour
{ . . . (생략) . . .
void Start()
{
// 2. 탄창을 총알 담을 수 있는 크기로 만들어 준다.
bulletObjectPool = new GameObject[poolSize];
// 3. 탄창에 넣을 총알 개수 만큼 반복하여
for (int i = 0; i < poolSize; i++)
{
// 4. 총알공장에서 총알 생성한다.
GameObject bullet = Instantiate(bulletFactory);
// 5. 총알을 오브젝트풀에 넣고싶다.
bulletObjectPool[i] = bullet;
// 비활성화 시키자.
bullet.SetActive(false);
}
}
- 반복적인 동작을 요하기 때문에 C# 에의 반복문 중 for 문을 사용하여 구현한다.
- 먼저 풀 크기만큼 반복문의 형체를 잡아 놓는다.
- 이미 PlayerFire 에 있는 Update 함수의 코드인 bullet 생성 코드를 가져온다.
총알 발사 로직을 수정하자.
오브젝트 풀을 활용하기 위해 기존 총알을 그때그때 만들어 발사하던 로직을 수정해야한다.
목표 : 발사 버튼을 누르면 탄창에 있는 총알 중 비활성화 된 녀석을 발사 하고 싶다.
순서 : 1. 발사 버튼을 눌렀으니까
2. 탄창 안에 있는 총알들 중에서
3. 비활성화 된 총알을
4. 총알을 발사하고 싶다.(활성화시킨다.)
public class PlayerFire : MonoBehaviour
{
. . . (생략) . . .
//목표 : 발사 버튼을 누르면 탄창에 있는 총알 중 비활성화 된 녀석을 발사 하고 싶다.
void Update()
{
//1.발사 버튼을 눌렀으니까
if (Input.GetButtonDown("Fire1"))
{
//2.탄창 안에 있는 총알들 중에서
for (int i = 0; i < poolSize; i++)
{ //3.비활성화 된 총알을 // - 만약 총알이 비활성화 되었다면
GameObject bullet = bulletObjectPool[i];
if (bullet.activeSelf == false)
{ //4.총알을 발사하고 싶다.(활성화시킨다.)
bullet.SetActive(true); // 총알을 위치 시키기
bullet.transform.position = transform.position;
//총알 발사 하였기 때문에 비활성화 총알 검색 중단
break;
}
}
}
}
}
PlayerFire 의 Update 함수의 구현 부를 다음과 같이 변경하여 준다.
총알 10개 생성!
- 실행을 해보면 적을 맞추었을 때 그 다음부터 총알이 잘 발사되지 않고 콘솔(Console) 창에 다음과 같은 오류 메시지가 표시 된다.
- 총알을 없애지 말고 다시 비활성화 시켜 탄창에 넣어주어야 하는 작업이 필요하다.
public class Enemy : MonoBehaviour
{ . . . (생략) . . .
private void OnCollisionEnter(Collision other)
{
. . . (생략) . . .
explosion.transform.position = transform.position;
// 만약 부딪힌 객체가 Bullet 인 경우에는 비활성화 시켜 탄창에 다시 넣어준다.
//1.만약 부딪힌 물체가 Bullet 이라면
if (other.gameObject.name.Contains("Bullet"))
{
//2.부딪힌 물체를 비활성화 other.gameObject.SetActive(false);
}
//3.그렇지 않으면 제거
else
{
Destroy(other.gameObject);
}
Destroy(gameObject);
}
}
- Enemy 클래스의 OnCollisionEnter 로 이동한다.
- “1. 만약 부딪힌 물체가 Bullet 이라면” 내용추가.
- "2. 부딪힌 물체를 비활성화” 코드 추가
- “3. 그렇지 않으면 제거” 의 구현 추가
이래도 나는 여전히 오류가 발생했다.
원인은 DestroyZone때문이었고, 아래와 같이 DestroyZone.cs코드를 수정해주었다.
- 총알이 날아갈 때 그림처럼 회전되어 이동하는 문제가 발생한다.
- Bullet 객체에 Rigidbody 컴포넌트가 붙어 있기 때문에 다른 물체와 부딪혀 회전값이 적용된 상태로 발사되기 때문이다.
- Rigidbody 가 이동 및 회전 처리를 못하도록 막아 해결한다. Bullet 프리팹을 더블 클릭하여 프리팹 편집 창으로 이동
- Bullet 객체의 Rigidbody 컴포넌트의 Constraints 속성을 체크하여 모두 잠가준다.
이렇게 오류없이 잘 실행되는 것을 확인할 수 있다.
SpawnPoint 만들기
- EnemyManager 게임오브젝트를 하이어라키에서 선택하고 를 눌러 하나 복제한다.
- EnemyManager 로 이름이 되어 있는 객체 빼고 나머지 뒤에 숫자가 붙은 EnemyManager 들은 모두 SpawnPoint 로 바꾸어 준다.
- SpawnPoint 들에 있는 EnemyManager 컴포넌트는 모두 제거한다.
속성 정의
public class EnemyManager : MonoBehaviour
{
//오브젝트풀 크기
public int poolSize = 10;
//오브젝트풀 배열
GameObject[] enemyObjectPool;
//SpawnPoint 들
public Transform[] spawnPoints;
. . . (생략) . . .
}
- 적들이 소환될 위치를 기억할 SpawnPoint 배열을 선언한다.
- 풀사이즈와 적들을 담을 오브젝트풀 맴버변수를 선언한다.
오브젝트풀 생성
public class EnemyManager : MonoBehaviour
{
. . . (생략) . . .
//1. 태어 날 때
void Start()
{
creatTime = Random.Range(1.0f, 5.0f);
//2. 오브젝트풀을 에너미들을 담을 수 있는 크기로 만들어 준다.
enemyObjectPool = new GameObject[poolSize];
//3. 오브젝트풀에 넣을 에너미 개수 만큼 반복하여
for (int i = 0; i < poolSize; i++)
{
//4. 에너미공장에서 에너미를 생성한다.
GameObject enemy = Instantiate(enemyFactory);
//5. 에너미를 오브젝트풀에 넣고싶다.
enemyObjectPool[i] = enemy;
// 비활성화 시키자.
enemy.SetActive(false);
}
}
1. Start 함수에서 태어날때 내용을 구성할 수 있도록한다.
2. 오브젝트풀을 에너미들을 담을 수 있는 크기로 만들어 준다.
3. 오브젝트풀에 넣을 에너미 개수 만큼 반복할 수 있도록 for 반복문을 활용한다.
4. 에너미공장에서 에너미를 생성한다.
5. 에너미를 오브젝트풀에 넣고 비활성화 시킨다.
오브젝트풀 활용하기
public class EnemyManager : MonoBehaviour
{
. . . (생략) . . .
void Update()
{
currentTime += Time.deltaTime;
//1.생성 시간이 되었으니까
if (currentTime > creatTime)
{
//2.에너미풀 안에 있는 에너미들 중에서
for (int i = 0; i < poolSize; i++)
{
//3.비활성화 된 에너미를 // - 만약 에너미가 비활성화 되었다면
GameObject enemy = enemyObjectPool[i];
if (enemy.activeSelf == false)
{
//4.에너미를 활성화 하고 싶다.
enemy.SetActive(true);
// 에너미 위치 시키기
enemy.transform.position = transform.position;
//에너미 활성화 하였기 때문에 검색 중단
break;
}
}
creatTime = Random.Range(1.0f, 5.0f);
currentTime = 0;
}
}
- Update 함수에서 오브젝트 풀을 활용할 수 있도록 수정한다.
- 에너미풀 안에 있는 적들 중에서 비활성화되어 있는 적을 반복적으로 검사 할 수 있도록 for 를 활용한다.
- 비활성화 된 에너미를 찾아 활성화 시켜준다.
public class Enemy : MonoBehaviour
{
. . . (생략) . . .
//1. 적이 다른 물체와 충돌 했으니까.
private void OnCollisionEnter(Collision other)
{
. . . (생략) . . .
//Destory 로 없애는 대신 비활성화 하여 풀에 자원을 반납합니다.
//Destroy(gameObject);
gameObject.SetActive(false);
}
}
- Enemy 클래스의 OnCollisionEnter 으로 이동한다.
- 비활성화 해 줌으로써 자원을 반납해 주도록 한다.
public class DestroyZone : MonoBehaviour
{
private void OnTriggerEnter(Collider other)
{
//1.만약 부딪힌 물체가 Bullet 이거나 Enemy 이라면
if (other.gameObject.name.Contains("Bullet") ||
other.gameObject.name.Contains("Enemy"))
{
//2.부딪힌 물체를 비활성화
other.gameObject.SetActive(false);
}
}
}
DestroyZone 에서는 Enemy 일 때의 조건을 하나 추가해 주도록 한다.
- 총알에서 처럼 에너미가 충돌하면 회전과 위치가 변화되지 않도록 Rigidbody 부분을 수정한다.
- [Project – Assets – Prefabs] 폴더에서 Enemy 프리팹을 선택
- 인스펙터 창에서 Rigidbody 컴포넌트의 Constraints 속성을 모두 체크하여 잠가 준다.
SpawnPoint 할당하기
- EnemyManager 객체를 선택하고 인스펙터 창을 잠가 준다.
- 하이어라키에서 SpawnPoint 객체를 모두 선택하여 EnemyManager 의 SpawnPoints 속성의 이름으로 드래그앤드롭 하여 할당한다.
- 등록이 완료되면 인스펙터의 우측 상단 자물쇠는 다시 원래대로 풀어 준다.
SpawnPoint 랜덤 할당
public class EnemyManager : MonoBehaviour
{
. . . (생략) . . .
void Update()
{
currentTime += Time.deltaTime;
//1.생성 시간이 되었으니까
if (currentTime > creatTime)
{
//2.에너미풀 안에 있는 에너미들 중에서
for (int i = 0; i < poolSize; i++)
{
. . . (생략) . . .
enemy.SetActive(true);
// 랜덤으로 인덱스 선택
int index = Random.Range(0, spawnPoints.Length);
// 에너미 위치 시키기
enemy.transform.position = spawnPoints[index].position;
. . . (생략) . . .
- EnemyManager.cs 스크립트로 이동
- 에너미 배치하는 곳의 코드를 Random.Range 함수를 이용하여 랜덤 인덱스를 뽑아 위치에 적용한다.
3. 오브젝트풀을 이용한 메모리 관리 (고급)
총알 오브젝트풀 리스트로 교체
public class PlayerFire : MonoBehaviour
{
public GameObject bulletFactory;
// 탄창에 넣을 총알 개수
public int poolSize = 10;
// 오브젝트풀 배열
public List <GameObject> bulletObjectPool;
. . . (생략) . . .
PlayerFire 스크립트로 이동한다.
기존에 사용하던 GameObject[] 배열 형식을 List로 교체한다.
public class PlayerFire : MonoBehaviour
{
void Start()
{
// 2. 탄창을 총알 담을 수 있는 크기로 만들어 준다.
bulletObjectPool = new List<GameObject>();
//3. 탄창에 넣을 총알 개수 만큼 반복하여
for (int i = 0; i < poolSize; i++)
{
// 4. 총알공장에서 총알 생성한다.
GameObject bullet = Instantiate(bulletFactory);
// 5. 총알을 오브젝트풀에 넣고싶다.
bulletObjectPool.Add(bullet);
// 비활성화 시키자.
bullet.SetActive(false);
}
}
Start 함수에서 배열로 되어 있는 처리를 모두 List로 바꿔준다.
public class PlayerFire : MonoBehaviour
{
. . . (생략) . . .
//목표 : 발사 버튼을 누르면 탄창에 있는 총알 중 비활성화 된 녀석을 발사 하고 싶다.
void Update()
{
//1.발사 버튼을 눌렀으니까
if (Input.GetButtonDown("Fire1"))
{
//2.탄창 안에 있는 총알이 있다면
if (bulletObjectPool.Count > 0)
{
//3.비활성화 된 총알을 하나 가져온다.
GameObject bullet = bulletObjectPool[0];
//4.총알을 발사하고 싶다.(활성화시킨다.)
bullet.SetActive(true);
//오브젝트풀에서 총알제거
bulletObjectPool.Remove(bullet);
// 총알을 위치 시키기
bullet.transform.position = transform.position;
}
}
}
- Update 함수에서의 발사 코드를 배열에서 List 로 수정한다.
- 배열은 오브젝트풀을 전수조사 하여 비활성화 객체를 찾아 냈다면 리스트에는 비활성화 객체만 들어있기 때문에 검색이 필요 없다.
- 리스트에 객체가 있는지 확인하고 있다면 가장 첫번째 요소를 가져와 사용
public class DestroyZone : MonoBehaviour
{
private void OnTriggerEnter(Collider other)
{
//1.만약 부딪힌 물체가 Bullet 이라면
if (other.gameObject.name.Contains("Bullet") ||
other.gameObject.name.Contains("Enemy"))
{
//2.부딪힌 물체를 비활성화
other.gameObject.SetActive(false);
//3. 부딪힌 물체가 총알일 경우 총알 리스트에 삽입
if (other.gameObject.name.Contains("Bullet"))
{
// PlayerFire 클래스 얻어오기
PlayerFire player =
GameObject.Find("Player").GetComponent<PlayerFire>();
// list 에 총알 삽입
player.bulletObjectPool.Add(other.gameObject);
}
}
}
}
다 사용한 총알을 다시 오브젝트풀에 넣어준다.
DestroyZone 스크립트로 이동하여 총알이 부딪혔을 경우 List 에 총알을 다시 넣는 코드를 추가한다.
public class Enemy : MonoBehaviour
{
//1. 적이 다른 물체와 충돌 했으니까.
private void OnCollisionEnter(Collision other)
{ . . . (생략) . . .
if (other.gameObject.name.Contains("Bullet"))
{
//2.부딪힌 물체를 비활성화
other.gameObject.SetActive(false);
// PlayerFire 클래스 얻어오기
PlayerFire player =
GameObject.Find("Player").GetComponent<PlayerFire>();
// list 에 총알 삽입 player.bulletObjectPool.Add(other.gameObject);
}
. . . (생략) . . .
}
}
Enemy 클래스로 이동하여 PlayerFire 객체를 얻어와서 bulletObjectPool 리스트에 값을 넣어준다.
에너미 오브젝트풀 리스트로 교체
public class EnemyManager : MonoBehaviour
{
//오브젝트풀 크기
public int poolSize = 10;
//오브젝트풀 배열
public List <GameObject>enemyObjectPool;
enemyObjectPool
- EnemyManager.cs 스크립트로 이동하여 GameObject[] enemyObjectPool 을 List 로 교체한다.
- 접근지시자는 public 으로 수정한다.
public class EnemyManager : MonoBehaviour
{
//1. 태어 날 때
void Start()
{
creatTime = Random.Range(minTime, maxTime);
//2. 오브젝트풀을 에너미들을 담을 수 있는 크기로 만들어 준다.
enemyObjectPool = new List<GameObject>();
//3. 오브젝트풀에 넣을 에너미 개수 만큼 반복하여
for (int i = 0; i < poolSize; i++)
{
//4. 에너미공장에서 에너미를 생성한다.
GameObject enemy = Instantiate(enemyFactory);
//5. 에너미를 오브젝트풀에 넣고싶다.
enemyObjectPool.Add(enemy);
// 비활성화 시키자.
enemy.SetActive(false);
}
}
Start 함수를 수정하여 객체 생성을 List 로 교체를 하고 값 삽입은 Add 함수를 이용하여 처리 한다.
public class EnemyManager : MonoBehaviour
{
void Update()
{
currentTime += Time.deltaTime;
//1.생성 시간이 되었으니까
if (currentTime > creatTime)
{
//2.오브젝트풀에 에너미가 있다면
GameObject enemy = enemyObjectPool[0];
if (enemyObjectPool.Count > 0)
{
//3.에너미를 활성화 하고 싶다.
enemy.SetActive(true);
//4.오브젝트풀에서 에너미제거
enemyObjectPool.Remove(enemy);
// 랜덤으로 인덱스 선택
int index = Random.Range(0, spawnPoints.Length);
// 5.에너미 위치 시키기
enemy.transform.position = spawnPoints[index].position;
}
. . . (생략) . . .
}
}
}
pdate 함수에서 for 문을 이용하여 조사하는 부분을 List 를 사용하도록 수정한다.
public class DestroyZone : MonoBehaviour
{
private void OnTriggerEnter(Collider other)
{
//1.만약 부딪힌 물체가 Bullet 이라면
if (other.gameObject.name.Contains("Bullet") ||
other.gameObject.name.Contains("Enemy"))
{
. . . (생략) . . . //3. 부딪힌 물체가 총알일 경우 총알 리스트에 삽입
if (other.gameObject.name.Contains("Bullet"))
{
. . . (생략) . . .
}
else if (other.gameObject.name.Contains("Enemy"))
{
// EnemyManager 클래스 얻어오기
GameObject emObject =
GameObject.Find("EnemyManager");
EnemyManager manager =
emObject.GetComponent<EnemyMAnager>();
// list 에 총알 삽입
manager.enemyObjectPool.Add(other.gameObject);
}
}
}
}
DestroyZone 스크립트로 이동하여 부딪힌 객체가 Enemy 일 때 EnemyManager 의 enemyObjectPool 변수를 가져와 그곳에 삽입한다.
public class Enemy : MonoBehaviour
{
//1. 적이 다른 물체와 충돌 했으니까.
private void OnCollisionEnter(Collision other)
{
. . . (생략) . . .
//Destroy(gameObject);
gameObject.SetActive(false);
// EnemyManager 클래스 얻어오기
GameObject emObject = GameObject.Find("EnemyManager");
EnemyManager manager =
emObject.GetComponent<EnemyManager> ();
// list 에 총알 삽입
manager.enemyObjectPool.Add(gameObject);
}
}
Enemy 클래스로 이동하여 부딪혔을 때 Enemy 를 EnemyManager 의 List 오브젝트풀에 다시 삽입해 준다.
하하 내용이 점점 어려워지는 것 같다!
'SWU_프로젝트 > GURU - unity' 카테고리의 다른 글
GURU_unity_3주차 FPS 게임 제작(2) (2) | 2023.07.10 |
---|---|
GURU_unity_2주차 FPS 게임 제작(1) (2) | 2023.07.07 |
GURU_unity_2주차 슈팅 게임 제작4 (2) | 2023.07.05 |
GURU_unity_2주차 슈팅 게임 제작3-2 (2) | 2023.07.05 |
GURU_unity_2주차 슈팅 게임 제작3-1 (0) | 2023.07.05 |