게임개발 3일차 (간단한 슈팅게임 1)
오늘부터는 벽에 갇힌 공간에서 적이 리스폰되고, 플레이어가 총을 쏴서 그적들을 제거하면서 살아남는 간단한 게임을 만들어보려고 한다.
1. 그래픽 에셋 다운
내가 사용할 그래픽을 만들어 놓은 사이트에서 다운을 받고
2. 그래픽 에셋 임포트
그걸 유니티에 넣어서 적절하게 자르고
3. 타일 맵
적절하게 잘린 것을 이용해 바닥과 벽을 구현하고
4. 플레이어 캐릭터 생성
플레이어 캐릭터 오브젝트를 생성하고
5. 입력과 컨트롤
입력과 컨트롤에 관한 스크립트를 짜주고
6. 충돌처리
플레이어가 벽과 충돌했을 때 멈추도록 설정하고
7. 애니메이션
걸어다닐 때 가는 방향대로 캐릭터가 보도록 설정하고 움직일 때는 애니메이션에 따라서 뛰도록 해주고
8. 총알 발사
총알 오브젝트를 만들고 벽에 닿으면 사라지도록 설정해보고
9. Prefab
Prefab이라는 설계도에서 instantiate(임시 생성)해서 발사할 수 있게 하고
10. 오브젝트 풀링
Instantiate()로 오브젝트를 만들고 Destroy()로 지우면 메모리 할당과 해제가 일어나는데, 이게 자주 반복되면 렉이 발생하거나 프레임이 뚝뚝 끊길 수 있으니 미리 만들어 놓고 재사용하는 오브젝트 풀링 기법을 배웠다.
이해는 잘 가는데 혼자서 해보라고 하면 할 자신이 없어서 내일 다시 해보려고 한다.
<PlayerController.cs>
using UnityEngine;
public class PlayerController : MonoBehaviour
{
public float speed = 3; // 🏃♂️ 플레이어 이동 속도 (public: 인스펙터에서 설정 가능)
Vector3 move; // 🔁 매 프레임마다 이동 방향을 저장할 벡터
public GameObject bulletPrefab; // 💣 총알 프리팹 (인스펙터에서 연결)
void Start()
{
// 시작할 때 필요한 초기화 작업 (현재는 없음)
}
// 🎮 매 프레임마다 호출됨 (플레이어 입력 및 애니메이션 처리)
void Update()
{
move = Vector3.zero; // 이동 초기화
// 왼쪽 입력 처리 (← 또는 A 키)
if (Input.GetKey(KeyCode.LeftArrow) || Input.GetKey(KeyCode.A))
{
move += new Vector3(-1, 0, 0);
}
// 오른쪽 입력 처리 (→ 또는 D 키)
if (Input.GetKey(KeyCode.RightArrow) || Input.GetKey(KeyCode.D))
{
move += new Vector3(1, 0, 0);
}
// 위쪽 입력 처리 (↑ 또는 W 키)
if (Input.GetKey(KeyCode.UpArrow) || Input.GetKey(KeyCode.W))
{
move += new Vector3(0, 1, 0);
}
// 아래쪽 입력 처리 (↓ 또는 S 키)
if (Input.GetKey(KeyCode.DownArrow) || Input.GetKey(KeyCode.S))
{
move += new Vector3(0, -1, 0);
}
move = move.normalized; // 대각선 이동 시 속도 보정 (길이를 1로 정규화)
// 왼쪽 이동 시 캐릭터 뒤집기
if (move.x < 0)
{
GetComponent<SpriteRenderer>().flipX = true;
}
// 오른쪽 이동 시 원래 방향으로
if (move.x > 0)
{
GetComponent<SpriteRenderer>().flipX = false;
}
// 이동 중이면 "Move" 애니메이션 트리거 실행해서 PlayerRun상태로 전이
if (move.magnitude > 0)
{
GetComponent<Animator>().SetTrigger("Move");
}
else // 멈췄을 때는 "Stop" 트리거 실행해서 PlayerIdle상태로 전이
{
GetComponent<Animator>().SetTrigger("Stop");
}
// 마우스 왼쪽 버튼 클릭 시 shoot() 호출
if (Input.GetMouseButtonDown(0))
{
shoot();
}
}
// 🔫 총알 발사 함수
void shoot()
{
// 마우스 위치를 월드 좌표로 변환
Vector3 worldPosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);
worldPosition.z = 0; // 2D 게임이므로 Z 좌표는 0
// 방향 벡터 계산 (마우스 방향 - 총알 발사 위치 보정)
worldPosition -= (transform.position + new Vector3(0, -0.5f, 0));
// 오브젝트 풀에서 총알 가져오기
GameObject newBullet = GetComponent<ObjectPool>().Get();
if (newBullet != null)
{
// 총알 위치 지정 (플레이어 아래쪽 살짝)
newBullet.transform.position = transform.position + new Vector3(0, -0.5f);
// 총알에 방향 설정
newBullet.GetComponent<Bullet>().Direction = worldPosition;
}
}
// 💨 물리 연산에 맞춰 이동 처리 (Update에서 직접 처리하면 버벅이게 됨)
private void FixedUpdate()
{
transform.Translate(move * speed * Time.fixedDeltaTime); // 실제로 움직이게 함
}
}
<Bullet.cs>
using UnityEngine;
public class Bullet : MonoBehaviour
{
public float speed = 10; // 💨 총알의 이동 속도
public float damage = 1; // 💥 총알이 가하는 데미지 (필요 시 사용 가능)
Vector2 direction; // 🔁 총알의 이동 방향 (private 형태로 내부 사용)
// 🔁 외부에서 방향을 설정할 수 있는 속성 (setter만 존재)
public Vector2 Direction
{
set
{
direction = value.normalized; // 방향 벡터 정규화 (길이를 1로 맞춤)
}
}
void Start()
{
// 초기화 코드 (현재는 없음)
}
// 🎮 매 프레임마다 호출됨 — 총알 이동 처리
void Update()
{
// 방향 * 속도 * 시간 → 프레임에 따라 일정하게 이동
transform.Translate(direction * speed * Time.deltaTime);
}
// 🧱 무언가에 닿았을 때 호출되는 함수 (트리거 충돌 감지)
private void OnTriggerEnter2D(Collider2D collision)
{
// 충돌한 오브젝트의 태그가 "Wall"이면
if (collision.tag == "Wall")
{
gameObject.SetActive(false); // 총알 비활성화 (오브젝트 풀링을 위한 처리)
}
}
}
<ObjectPool.cs>
using System.Collections.Generic; // List 사용을 위해 필요
using UnityEngine;
public class ObjectPool : MonoBehaviour
{
public GameObject prefab; // 🔁 생성할 오브젝트(예: 총알)의 프리팹
public int maxObject = 30; // 🧮 최대 풀 개수
public Transform parent; // 🧱 풀 오브젝트들의 부모가 될 트랜스폼 (정리용)
List<GameObject> pool; // 📦 풀에 담긴 오브젝트 목록
// ☝️ 게임 시작 시 최초 1회 실행
void Start()
{
pool = new List<GameObject>(); // 리스트 초기화
// 최대 개수만큼 오브젝트 미리 생성해서 풀에 보관
for (int i = 0; i < maxObject; i++)
{
GameObject obj = Instantiate(prefab, parent); // 프리팹 생성 후 parent 밑에 배치
obj.SetActive(false); // 처음엔 꺼둠 (비활성 상태)
pool.Add(obj); // 풀에 추가
}
}
// 🧲 오브젝트를 꺼내서 사용할 때 호출
public GameObject Get()
{
// 풀에서 비활성화된 오브젝트가 있는지 확인
foreach (GameObject obj in pool)
{
if (!obj.activeInHierarchy) // 현재 씬에서 꺼져있는 오브젝트라면
{
obj.SetActive(true); // 다시 활성화해서
return obj; // 반환
}
}
return null; // 사용 가능한 오브젝트가 없을 경우 null 반환
}
// Update 함수는 필요 없으므로 삭제하거나 비워둠
}
참고로 public으로 지정하면 유니티의 인스펙터에서 직접 바꿀 수 있는데 이 때는 코드보다 인스펙터에서 지정한 값이 우선된다.
오브젝트가 몸체를 가지고 충돌, 닿을때의 이벤트를 정하고 싶으면 Rigidbody 컴포넌트와 Collider 컴포넌트를 넣어줘야 한다. 콜라이더는 충돌영역을 설정하고 리지드바디는 그 때의 물리적 움직임을 정한다.
Sorting Layer로 전체적인 층을 정할 수 있고 (백그라운드는 0, 캐릭터는 1 등등)
그 내부에서도 Order in Layer로 누가 더 위에 있을 지 정할 수 있다.
각 오브젝트마다 태그를 붙여야 나중에 if (collision.tag == "Wall") 등에 쓰일 수 있다.