유니티 게임 개발 무료 강의 - 네 번째 따라하기
강의 링크 (영상 22 개)
무한 맵 이동 - 타일 그리기
2D 타일 맵으로 지형을 깔아보자!
Window > 2D > Tile Palette 로 팔레트 창 열기
Assets - Undead Survivor - Tiles 폴더 선택 상태에서
Project 의 "+" 버튼
인접한 타일에 따라 이미지가 정해지는 타일
Output 을 Random 으로 할 것이기 때문에
Number of Tilling Rules 은 1 로함
랜덤이기 때문에 타일 종류별로 흔하게 나오는 것들을 많이 넣는 방식
이제 완성된 타일 에셋을 팔레트에 드래그드랍하여 덮어씌우기...
맵에 타일을 깔기 위해서
Hierarchy 에서 2D Object > TileMap > Rectanglar
Title Palatte 에서 Line Brush 를 이용해서 전체 틀을 그리고 채워넣기로 채운다...
Brush 종류
Line Brush 는 시작점부터 커서 따라서 그려줌
실수로 잘못 그리면, Tool 에 지우개가 있음
룰 타일의 Noise 수치를 조절하여 원하는 형태의 타일맵 제작이 가능
재배치 이벤트 준비
플레이어가 이동하는 경우 타일맵도 이동시켜 주도록 한다... (플레이어와 거리가 멀어지면 재배치?)
Tilemap 게임오브젝트에 2가지 컴포넌트를 추가해준다
물리적인 충돌 트리거를 위해서 Tilemap Collider 2D
하지만, 개별 사각별(유닛) 별로 충돌이 발생하기 때문에?
하나로 합치기 위해서 Composite Collider 2D 를 추가해서 사용
❗강의 에서는 Used by Composite 에 체크 이지만,
6000.1 버전에서는 Composite Operation 으로 바뀌고, 선택할 수가 있는데, Merge 로 선택하면 됨
Merge 로 선택하게 되면, Tilemap Collider 2D 에 있는 Is Trigger 는 숨겨지고,
Composite Collider 2D 에서 Is Trigger 를 체크해두면 됨
Rigidbody 2D 에서 Body Type 을 Dynamic 에서 Static 으로 변경하면 됨
Gemini 에게 왜 바꿨는지 물어보니.. 상세히 알려주는데, 요약하면
Composite Collider 2D에 Rigidbody 2D (Static)을 적용하면, 해당 Composite Collider는 움직이지 않는 단단한 충돌 영역이 되어 다른 물리 오브젝트들과 상호작용합니다. 이는 지형지물이나 고정된 장애물을 효율적으로 표현하고 관리하는 데 매우 유용합니다.
Dynamic Body Type과는 달리, Static Body Type은 물리적인 움직임 없이 충돌 감지 및 반응만을 수행한다는 점을 기억하시면 됩니다.
Player 의 Child Object 로 Empty Object 를 추가해서 Area 로 이름을 바꾸고, Box Collider 2D component 를 추가한다. Size 의 값은 Timemap 의 크기와 동일하게 20 (유닛 20 칸)
재배치 스크립트
Reposition.cs / GameManager.cs 를 추가한다...
Reposition 의 경우에 Tilemap 에 할당이 되고, Reposition 에서 Player 에 대한 접근이 필요한데, 직접 Player 에 접근하는 것이 아니라 GameManager 를 통해서 접근하도록 한다
Reposition 에서 GameManager 를 이용할 수 있는 방법
1. 장면이 여러 개이면 싱글톤 처리
2. 장면이 하나라서 메모리에 올려서(static) 처리
- static 은 public 이더라도 Inspector 에서 보이지 않음
GameManager.cs
public class GameManager : MonoBehaviour
{
public static GameManager instance;
public Player player;
void Awake()
{
instance = this;
}
}
Reposition.cs
public class Reposition : MonoBehaviour
{
void Start()
{
GameManager.instance.player
}
}
GameManager.instance 를 통해서 player GameObject 를 사용가능
재배치 로직 만들기
스냅핑 이동 : Ctrl + 기즈모 드래그
(값 설정은 6000.1 에서 찾을 수가 없어서 못함 😨)
타일 맵 네 장으로 (가로, 세로 40 유닛) 전체 맵을 만든다..
Player 의 Area 는 내부에 (가로, 세로 20 유닛)
Reposition.cs
public class Reposition : MonoBehaviour
{
void OnTriggerExit2D(Collider2D collision)
{
if (!collision.CompareTag("Area"))
return;
Vector3 playerPos = GameManager.instance.player.transform.position;
Vector3 myPos = transform.position;
float diffX = Mathf.Abs(playerPos.x - myPos.x);
float diffY = Mathf.Abs(playerPos.y - myPos.y);
Vector3 playerDir = GameManager.instance.player.inputVec;
float dirX = playerDir.x < 0 ? -1 : 1;
float dirY = playerDir.y < 0 ? -1 : 1;
switch (transform.tag)
{
case "Ground":
if (diffX > diffY)
{
transform.Translate(Vector3.right * dirX * 40);
}
else if (diffX < diffY)
{
transform.Translate(Vector3.up * dirY * 40);
}
break;
case "Enemy":
break;
}
}
}
Player 의 이동 방향에 따라서 맵을 이동시켜 주기 때문에 무한한 맵으로 보인다...
하지만, 이 방식이 꼭 정답은 아니다라고 하심!
카메라 설정
두 가지 문제 해결 필요,
- 이동시 카메라가 플레이어를 따라가며 보여주지 않음
- 화면에 흰색 선이 보임
Main Camera 에 Pixel Perfect Camera component 를 추가해준다 (URP 버전으로)
화면에 에러가 나는데,
Assets Pixels Per Unit 값을 100 에서 현재 사용하고 있는 값인 18 로 맞춰준다
해상도를
짝수로 맞추기 위해서 Game 창을 늘이거나 줄인다 (흰 선 제거)
Reference Resolution : 카메라 크기 계산을 위한 참고 해상 (값이 크면 화면이 작아진다)
주의: 타일맵 크기, Area 크기, 재배치 거리는 카메라 크기와 비슷하게!
이제 카메라를 처리
Window - Package Manager 에서 Cinemachine 설치
Hierarchy 의 + 버튼 선택해 보면
6.1 에서는 Virtual Camera 가 없다 (6.1 차이점)
Google 검색과 Gemini 이용해 보니 첫 번째
Cinemachine Camera 쓰면 된다고 함
Virtual Camera 는 카메라 감독 역할을 수행
6.1 에서 Follow 라는 메뉴는 없고,
Tracking Target 에 Player 를 할당
메뉴 설명이 Object for the camera to follow 라는 것을 보니 맞는듯......
그리고, Position Control 의 값도 Follow 로 바꿔주어야 강의 내용 처럼 맵이 따라 움직임 (6.1 차이점)
움직임을 맞춰주기 위해서 Main Camera componet 의 Update Method 값을 Fixed Update 로 바꿔준다
캐릭터 그림자를 잘 보이도록 하기 위해서 타일맵의 Order in Layer 값을 -1 로 해준다
7번째 영상 몬스터 만들기
Sprite 폴더에서 Enemy 1 의 Run 0 번 이미지를 드래그 해서 Enemy GameObject 를 만든다..
Enemy 의 Order in Layer 값은 2
Child Object 로 Shadow 를 추가해주고, Order in Layer 값 변경 대신에 Y 값을 -0.45 로 한다
적의 경우에 만들어 이미 만들 둔 Animation Controller 를 지정해준다
이제 Rigidbody 2D componet 추가해서 중력과 회전을 제거(부딪힐때 회전을 하면 안되게...)
Freeze Rotation 을 체크
Capsule Collider 2D 영역이 조금 맞지 않기 때문에
값을 0.7 과 0.9 로 변경하면 정확히 일치
Player 의 경우에 Weight 를 수정해 준다. (1 => 5)
플레이어 추적 로직
플레이어가 멀어지면 타일맵 재배치하듯이 Enemy 도 재배치하도록 한다.
Reposition.cs
public class Reposition : MonoBehaviour
{
Collider2D coll;
void Awake()
{
coll = GetComponent<Collider2D>();
}
void OnTriggerExit2D(Collider2D collision)
{
...
switch (transform.tag)
{
case "Ground":
...
break;
case "Enemy":
if (coll.enabled)
{
transform.Translate(playerDir * 20 + new Vector3(Random.Range(-3f, 3f), Random.Range(-3f, 3f), 0));
}
break;
}
}
}
Collider2D 클래스는 모든 Collider 클래스의 Parent 클래스...
8 번째 영상
프리펩에 게임 오브젝트로 등록하는 경우에
씬에 배치된 해당 여러 오브젝트의 Scale 값을 동시에 적용할 수 있다
Scale 에 있는 이 버튼을 아래 상태고 값 변경시에 모두 동시에 적용
오브젝트 풀 만들기
✅ 유니티는 생성 Instantiate 와 삭제 Destory 함수를 제공
주의 : Instantiate + Destroy 함수를 너무 자주 사용하면 메모리 문제 발생 가능 (성능 이슈?)
(메모리 파편화/가비지 컬렉션 문제??)
- PoolManager 게임 오브젝트를 만들어서 프리펩들을 저장할 배열 변수 선언?
- Instantiate 는 원본 오브젝트를 복제하여 장면에 생성하는 함수
PoolManager.cs
public class PoolManager : MonoBehaviour
{
// .. 프리펩들을 보관할 변수
public GameObject[] prefabs;
// .. 풀 담당을 하는 리스트들
List<GameObject>[] pools;
void Awake()
{
pools = new List<GameObject>[prefabs.Length];
for (int index = 0; index < prefabs.Length; index++)
{
pools[index] = new List<GameObject>();
}
Debug.Log(pools.Length);
}
public GameObject Get(int index)
{
GameObject select = null;
// ... 선택한 풀의 놀고 있는 (비활성화 된) 게임 오브젝트 접근
foreach (GameObject item in pools[index])
{
if (!item.activeSelf)
{
// ... 발견하면 select 변수에 할당
select = item;
select.SetActive(true);
break;
}
}
// ... 못 찾았으면?
if (!select)
{
// ... 새롭게 생성하고 select 변수에 할당
select = Instantiate(prefabs[index], transform);
pools[index].Add(select);
}
return select;
}
}
풀링 사용해보기
플레이어에 소환 담당 자식 오브젝트 및 스크립트 추가
우선, GameManager.cs 에 PoolManager 의 public 변수를 만들어서 PoolManager 게임 오브젝트를 지정해준다
Spawner.cs 에서
GameManager.instance.... 으로 편하게 처리할 수 있다
Enemy.cs 에서
객체가 생성될 때 Target 에 대한 설정을 위해서
OnEnable() 메소드(스크립트가 활성화 될 때, 호출되는 이벤트 함수)를 추가한다.
void OnEnable()
{
target = GameManager.instance.player.GetComponent<Rigidbody2D>();
}
Prefabs 에 있는 Enemy 의 경우에 Inspector 창에서 target 설정을 Hierarchy 로부터 Player 를 바로 지정하지 못한다. 그래서, 코드로 Enable 되는 시점에 target 을 지정
Spawner.cs 의 테스트 코드
스페이스바를 누르면 플레이어 인근에 1 번 Enemy 생성
public class Spawner : MonoBehaviour
{
void Update()
{
if (Input.GetButtonDown("Jump"))
{
GameManager.instance.pool.Get(1);
}
}
}
주변에 생성하기
Point 의 Icon 색상을 변경하고, 크게 해서 잘 보이게 할 수 있다
Enemy 를 생성하는 Point 를 16 개를 만들었기 때문에 배열로 만들어준다
GetComponentsInChildren<Transform >() 을 이용한다
Transform 을 가지고 와서 생성하는 위치를 지정
return 시에는 Parent 객체의 Transform 도 포함이라서 전체 개수는 Child 개수 + 1
public class Spawner : MonoBehaviour
{
public Transform[] spawnPoint;
float timer;
void Awake()
{
spawnPoint = GetComponentsInChildren<Transform>();
}
void Update()
{
timer += Time.deltaTime;
if (timer > 0.2f)
{
timer = 0;
Spawn();
}
}
void Spawn()
{
GameObject enemy = GameManager.instance.pool.Get(Random.Range(0, 2));
enemy.transform.position =
spawnPoint[Random.Range(1, spawnPoint.Length)].position;
}
}
9 번째 영상 - 소환 레벨 적용하기
시간에 따른 난이도를 위해서 세 가지(?) 스크립트 수정이 필요
먼저, GameManager.cs 수정
gameTime 과 maxGameTime 을 추가
Update() 메소드에서 시간을 더해준다
public class GameManager : MonoBehaviour
{
public static GameManager instance;
public float gameTime;
public float maxGameTime = 2 * 10f;
public Player player;
public PoolManager pool;
void Awake()
{
instance = this;
}
void Update()
{
gameTime += Time.deltaTime;
if (gameTime > maxGameTime)
{
gameTime = maxGameTime;
}
}
}
Spawner.cs 수정
public class Spawner : MonoBehaviour
{
public Transform[] spawnPoint;
int level;
float timer;
void Awake()
{
spawnPoint = GetComponentsInChildren<Transform>();
}
void Update()
{
timer += Time.deltaTime;
level = Mathf.FloorToInt(GameManager.instance.gameTime / 10f);
if (timer > (level == 0 ? 0.5f : 0.2f))
{
timer = 0;
Spawn();
}
}
void Spawn()
{
GameObject enemy = GameManager.instance.pool.Get(level);
enemy.transform.position = spawnPoint[Random.Range(1, spawnPoint.Length)].position;
}
}
Mathf.FloorToInt() => 소수점 아래는 버리고 Int 형으로
Mathf.CeilToInt() => 소수점 아래를 올리고 Int 형으로
level 에 따라 소환되는 Enemy 가 바뀌도록 수정
SpawnData 라는 class 를 추가하는데,
public class SpawnData
{
public int spriteType;
public float spawnTime;
public int health;
public float speed;
}
public class Spawner : MonoBehaviour
{
...
public SpawnData[] spawnData;
public 으로 추가해도 Inspector 에서 바로 보이지 않는다.
이 경우에는 직렬화(객체를 저장 혹은 전송하기 위해 변환) 처리가 필요하다
[System.Serializable]
public class SpawnData
{
...
}
이제 인스펙터에서 초기화 가능하게 바뀐다
몬스터 다듬기
Prefabs 에 Enemy 를 하나만 두고 AnimatorController 를 code 에서 처리하도록 하기 위해서는 멤버 변수로 RuntimeAnimatorController 를 사용해야 한다
Enemy.cs
public class Enemy : MonoBehaviour
{
public float speed;
public float health;
public float maxHealth;
public RuntimeAnimatorController[] animCon;
public Rigidbody2D target;
bool isLive;
Rigidbody2D rigid;
Animator anim;
SpriteRenderer spriter;
void Awake()
{
rigid = GetComponent<Rigidbody2D>();
anim = GetComponent<Animator>();
spriter = GetComponent<SpriteRenderer>();
}
// Update is called once per frame
void FixedUpdate()
{
if (!isLive)
{
return;
}
Vector2 dirVec = target.position - rigid.position;
Vector2 nextVec = dirVec.normalized * speed * Time.deltaTime;
rigid.MovePosition(rigid.position + nextVec);
// rigid.velocity = Vector2.zero;
rigid.linearVelocity = Vector2.zero;
}
void LateUpdate()
{
spriter.flipX = target.position.x < rigid.position.x;
}
void OnEnable()
{
target = GameManager.instance.player.GetComponent<Rigidbody2D>();
isLive = true;
health = maxHealth;
}
public void Init(SpawnData data)
{
anim.runtimeAnimatorController = animCon[data.spriteType];
speed = data.speed;
maxHealth = data.health;
health = data.health;
}
}
네 번째 시간 - 소환 적용하기
현재 코드에서 index 오류가 발생하는데, 아래와 같이 코드를 바꿔준다.
level = Mathf.Min(Mathf.FloorToInt(GameManager.instance.gameTime / 10f),
spawnData.Length - 1);
if (timer > spawnData[level].spawnTime)
spawnData 가 2 개 인데, level 이 계속 오르게 되면 index 범위를 벗어나게 된다.
10 번째 영상 - 회전하는 근접무기 구현
- 데미지와 관통 처리 변수
public class Bullet : MonoBehaviour
{
public float damage;
public int per;
public void Init(float damage, int per)
{
this.damage = damage;
this.per = per;
}
}
Enemy.cs
- OnTriggerEnter2D 매개변수의 태그를 조건으로 활용
public class Enemy : MonoBehaviour
{
...
void OnTriggerEnter2D(Collider2D collision)
{
if (!collision.CompareTag("Bullet"))
return;
health -= collision.GetComponent<Bullet>().damage;
Debug.Log(health);
if (health > 0)
{
// .. Live, Hit Action
}
else
{
// .. Die
Dead();
}
}
void Dead()
{
gameObject.SetActive(false);
}
}
세 번째 시간 - 근접무기 생성
Player 게임 오브젝트에 Weapon 0 게임 오브젝트 추가하고, Weapon.cs 추가
근접무기 프리펩의 Order in Layer 를 몬스터(2)보다 높여서 무기(3)가 잘 보이도록 한다
public class Weapon : MonoBehaviour
{
public int id;
public int prefabId;
public float damage;
public int count;
public float speed;
void Start() {
Init();
}
void Update() {
switch (id) {
case 0:
transform.Rotate(Vector3.back * speed * Time.deltaTime);
break;
default:
break;
}
}
public void Init() {
switch (id) {
case 0:
speed = 150;
Batch();
break;
default:
break;
}
}
void Batch() {
for (int index = 0; index < count; index++) {
Transform bullet = GameManager.instance.pool.Get(prefabId).transform;
bullet.parent = transform;
bullet.GetComponent<Bullet>().Init(damage, -1); // -1 is Infinity per.
}
}
}
네 번째 시간 - 근접무기 배치
Weapon.cs 코드
- 강의 내용에는 없는 코드가 1 줄 추가되어 있는데,
Bullet 이 Player 바로 위에 생성되지 않고, 좀 다른 곳에 생성이 되면
아래 코드를 추가해주면 된다.
6000.1 버전만의 문제일 수도..
bullet.transform.position = transform.position;
public class Weapon : MonoBehaviour
{
...
void Batch()
{
for (int index = 0; index < count; index++)
{
Transform bullet = GameManager.instance.pool.Get(prefabId).transform;
bullet.parent = transform;
bullet.transform.position = transform.position;
Vector3 rotVec = Vector3.forward * 360 * index / count;
bullet.Rotate(rotVec);
bullet.Translate(bullet.up * 1.5f, Space.World);
bullet.GetComponent<Bullet>().Init(damage, -1); // -1 is Infinity per.
}
}
}
다섯번째 시간 - 레벨에 따른 배치
Input System 을 적용해도 기존 Input 을 사용하려면 Player 설정에서 확인할 것이 있다
// .. Test Code
if (Input.GetButtonDown("Jump"))
{ ... }
Edit > Project Settings... > Player > Other Settings 에서
Active Input Handling 이 "Both" 로 설정되어 있어야 함
기존 코드에서 업데이트 된 내용
- 기존 오브젝트를 먼저 활용하고 모자란 것은 풀링에서 가지고 온다 (if 분기처리)
- bullet 의 위치, 회전 초기화가 필요
void Batch()
{
for (int index = 0; index < count; index++)
{
Transform bullet;
if (index < transform.childCount)
{
bullet = transform.GetChild(index);
}
else
{
bullet = GameManager.instance.pool.Get(prefabId).transform;
bullet.parent = transform;
}
// bullet.transform.position = transform.position;
bullet.localPosition = Vector3.zero;
bullet.localRotation = Quaternion.identity;
...
}
}
현재 진도 10 / 22
배우는게 너무 많아서.... 앞 내용이 기억이...
댓글
댓글 쓰기