말씀하신 대로 **"프리팹을 그냥 랜덤한 위치에 배치하고, 충돌 처리로 겹치지 않게 한다"**는 방식은 몇 가지 심각한 문제를 일으킵니다.
- 성능 저하: 오브젝트를 하나 생성할 때마다 주변의 모든 다른 오브젝트와 충돌하는지 검사해야 합니다. 맵이 커질수록 기하급수적으로 느려집니다.
- 길 막힘: 운이 나쁘면 플레이어가 지나갈 수 없는 형태로 지형이 생성될 수 있습니다.
- 부자연스러움: 완전히 랜덤하게 배치하면 오브젝트들이 의미 없이 흩어져 있거나, 어색하게 뭉쳐있게 됩니다. 동굴이나 절벽 같은 '의도된 지형'을 만들 수 없습니다.
따라서 현대적인 랜덤 맵 생성은 '단순 배치'가 아니라, '규칙에 기반한 절차적 생성(Procedural Generation)' 방식을 사용합니다.
복잡하게 들리지만, 기본적인 아이디어는 간단합니다. "보이지 않는 격자(Grid)를 그리고, 각 칸에 무엇을 채울지 규칙에 따라 결정한다" 입니다.
가장 쉽고 강력한 랜덤 맵 구현 방법: '청크(Chunk)' 기반 생성
수직으로 무한히 내려가는 다이빙 게임에 가장 적합한 방식입니다.
1. 기본 개념
- 프리팹 (Prefab): 맵을 구성하는 조각들입니다. 바위1, 바위2, 해초, 산호, 빈 공간 등 다양한 종류의 '레고 블록'을 미리 만들어 둡니다.
- 그리드 (Grid): 게임 월드를 눈에 보이지 않는 바둑판으로 나눕니다. 한 칸의 크기는 기본 블록(프리팹)의 크기와 맞추는 것이 편합니다. (예: 1m x 1m)
- 청크 (Chunk): 맵을 일정한 크기의 '구역'으로 나눕니다. 예를 들어 가로 20칸, 세로 50칸짜리 그리드를 하나의 '청크'라고 부릅니다. 맵은 이 청크들이 위아래로 계속 이어붙여진 형태입니다.
2. 동작 방식 (가장 중요!)
- 초기 생성: 게임이 시작되면, 플레이어 주변에 몇 개의 청크(예: 3개)를 생성해서 배치합니다.
- [청크 1]
- [청크 2 (플레이어 시작 위치)]
- [청크 3]
- 플레이어 위치 감지: 게임은 계속 플레이어의 위치를 확인합니다.
- 동적 생성 및 삭제:
- 플레이어가 아래로 내려가 청크 3의 영역으로 진입하는 순간, 보이지 않는 더 아래쪽에 청크 4를 새로 생성합니다.
- 동시에, 플레이어에게서 너무 멀어져 보이지 않게 된 맨 위쪽의 **청크 1은 메모리에서 삭제(Destroy)**합니다.
- 결과:
- [청크 2]
- [청크 3 (플레이어 현재 위치)]
- [청크 4] ← 새로 생성됨
- (청크 1은 삭제됨)
이 방식을 사용하면, 실제로는 3~4개의 청크만 존재하지만 플레이어는 마치 무한한 바닷속을 탐험하는 것처럼 느끼게 됩니다. 이것이 '무한 맵'의 핵심 원리이며, 성능을 아낄 수 있는 가장 좋은 방법입니다.
그렇다면 '청크' 내부는 어떻게 채우는가? (랜덤 생성의 핵심)
MapGenerator라는 스크립트가 특정 청크를 생성하라는 명령을 받으면, 다음과 같은 규칙으로 내부를 채웁니다.
- 기본 지형 생성 (Perlin Noise 활용):
- '펄린 노이즈(Perlin Noise)'라는 수학 공식을 사용하면 자연스러운 동굴이나 지형을 만들 수 있습니다. 간단히 말해 **'자연스러운 모양의 랜덤 값'**을 만들어주는 기술입니다.
- 청크의 모든 그리드 칸(x, y)을 순회하면서 펄린 노이즈 값을 계산합니다.
- if (노이즈값 > 0.6) 이면 그 칸에 바위 프리팹을 배치하고, 아니면 빈 공간으로 둡니다. 이렇게 하면 구불구불한 동굴 벽 같은 형태가 자연스럽게 만들어집니다.
- 자원 및 오브젝트 배치:
- 일단 기본 지형(바위, 빈 공간)이 배치되면, 이제 빈 공간에 자원을 뿌립니다.
- for 루프로 모든 빈 공간 칸을 돌면서, 낮은 확률로 자원을 배치합니다.
- if (Random.value < 0.05) 이면 그 칸에 해초 프리팹을 배치합니다. (5% 확률)
- if (Random.value < 0.01) 이면 희귀 광물 프리팹을 배치합니다. (1% 확률)
- 깊이에 따른 변화:
- MapGenerator는 현재 생성하려는 청크의 **'깊이'**를 알고 있습니다.
- 이 깊이 값에 따라 사용하는 프리팹의 종류와 생성 확률을 다르게 설정합니다.
- 얕은 곳(0m~100m) 청크: 산호 프리팹과 열대어 프리팹을 높은 확률로 생성.
- 깊은 곳(100m~500m) 청크: 바위 프리팹의 비율을 높이고, 발광석 프리팹을 낮은 확률로 생성.


네, 그럼요! 그리고 "뱀파이어 서바이벌"을 예시로 드신 것은 정말 완벽한 비유입니다. 정확히 그런 느낌을 만드는 것이 맞습니다.
플레이어가 화면 중앙에 고정된 것처럼 보이고, 플레이어가 움직이면 주변 배경(맵)이 스크롤되는 방식이죠. 기술적으로는 **'카메라가 플레이어의 위치를 매 순간 똑같이 따라가도록 만드는 것'**입니다.
Vector3 newPosition = new Vector3(target.position.x, target.position.y, transform.position.z);
// 계산된 새 위치로 카메라를 이동시킵니다.
transform.position = newPosition;
1. 느낌의 문제 해결하기: '패럴랙스'란 무엇인가?
차를 타고 창밖을 볼 때를 생각해보세요.
- 가까이 있는 가드레일이나 나무는 '휙휙' 하고 아주 빨리 지나갑니다.
- 멀리 있는 산이나 구름은 아주 천천히 움직이는 것처럼 보입니다.
이 현상이 바로 '시차(Parallax)'입니다. 이 원리를 게임에 적용하면, 여러 겹의 배경을 서로 다른 속도로 움직이게 해서 엄청난 깊이감과 속도감을 만들어낼 수 있습니다.
우리는 딱 2겹만으로도 이 효과를 낼 수 있습니다.
- 배경: 플레이어보다 훨씬 느리게 움직임 (멀리 있는 바다 속 풍경)
- 플레이어: 정상 속도로 움직임
2. 기술적인 문제 해결하기: 배경은 어떻게 넣는가?
연극 무대를 생각하시면 쉽습니다. 배우(플레이어) 뒤에 배경 그림이 있고, 배우 앞에 작은 소품이 있을 수 있죠. 유니티는 이것을 **'정렬 레이어(Sorting Layers)'**로 관리합니다.
[1단계] 배경 이미지 준비 및 배치
① 임시 배경 이미지 구하기: 인터넷에서 'underwater background seamless' 또는 'ocean texture tileable' 같은 키워드로 검색해서 위아래로 계속 이어붙여도 자연스러운 이미지를 하나 구하거나, 그림판으로 파란색 그라데이션 이미지를 간단하게 만들어도 좋습니다.
② Sprites 폴더에 이미지 넣기: 다운받은 이미지를 프로젝트의 Sprites 폴더로 끌어다 놓습니다.
③ 배경 오브젝트 생성:
- InGameScene의 하이어라키 창에서 우클릭 > 2D Object > Sprite를 선택합니다.
- 이름을 Background로 바꿔줍니다.
- Background 오브젝트를 선택하고 인스펙터 창을 보면 Sprite Renderer 컴포넌트가 있습니다. Sprite 슬롯에 방금 가져온 배경 이미지를 끌어다 놓으세요.
- 아마 화면이 배경으로 꽉 찰 겁니다. Transform의 Scale 값을 조절해서 화면에 적당히 보이도록 크기를 맞춰주세요.
[2단계] 정렬 레이어(Sorting Layer) 설정 (가장 중요!)
지금은 플레이어와 배경이 같은 '도화지'에 그려져서 누가 앞에 나올지 알 수 없습니다. 도화지를 여러 장 겹쳐서 순서를 정해줘야 합니다.
① 레이어 생성:
- Background 오브젝트를 선택한 상태에서, Sprite Renderer 컴포넌트의 Sorting Layer 드롭다운 메뉴를 클릭합니다. (기본값은 Default)
- **Add Sorting Layer...**를 선택합니다.
- 새로운 창이 뜨면 + 버튼을 눌러 레이어를 2개 추가합니다.
- 각각 이름을 **Background**와 **Player**로 지어줍니다.
- 순서가 중요합니다! Background가 위에, Player가 아래에 오도록 순서를 맞춰주세요. (위에 있을수록 뒤에 그려짐)
② 레이어 적용:
- 다시 하이어라키 창에서 Background 오브젝트를 선택하고, Sprite Renderer의 Sorting Layer를 방금 만든 **Background**로 설정합니다.
- Player 오브젝트를 선택하고, Sprite Renderer의 Sorting Layer를 **Player**로 설정합니다.
이제 유니티는 항상 Background를 먼저 그린 다음, 그 '앞에' Player를 그리게 됩니다.
[3단계] 패럴랙스 스크롤링 스크립트 작성
카메라가 움직일 때, 배경이 카메라보다 '살짝 덜' 움직이게 만들면 됩니다.
① Scripts 폴더에 ParallaxBackground 라는 이름의 새 C# 스크립트를 만듭니다.
② 아래 코드를 복사해서 붙여넣으세요.
using UnityEngine;
public class ParallaxBackground : MonoBehaviour
{
private Transform cameraTransform;
private Vector3 lastCameraPosition;
// 이 값이 작을수록 배경이 더 멀리 있는 것처럼 천천히 움직입니다.
// 0.5는 카메라 이동 거리의 50%만 따라가겠다는 의미입니다.
[SerializeField] private float parallaxEffectMultiplier = 0.5f;
void Start()
{
// Main Camera의 Transform 정보를 가져옵니다.
cameraTransform = Camera.main.transform;
// 시작할 때의 카메라 위치를 저장합니다.
lastCameraPosition = cameraTransform.position;
}
void LateUpdate()
{
// 카메라가 이전 프레임에 비해 얼마나 움직였는지 계산합니다.
Vector3 deltaMovement = cameraTransform.position - lastCameraPosition;
// 배경을 카메라 이동량의 일부만큼만 움직입니다.
transform.position += deltaMovement * parallaxEffectMultiplier;
// 다음 프레임에서 계산할 수 있도록 현재 카메라 위치를 다시 저장합니다.
lastCameraPosition = cameraTransform.position;
}
}
③ 스크립트 적용:
- Scripts 폴더의 ParallaxBackground.cs 스크립트를 하이어라키 창의 Background 오브젝트에 끌어다 붙입니다.
- Background 오브젝트의 인스펙터 창을 보면 Parallax Effect Multiplier라는 값이 보일 겁니다. 일단 0.5 정도로 두고 테스트해보세요.
이제 게임을 실행하고 플레이어를 움직여보세요! 플레이어가 움직일 때 배경이 미끄러지듯 느리게 따라오면서, 이전과는 비교도 안 되는 깊이감과 속도감을 느끼실 수 있을 겁니다.
1. 궁금증: 중심좌표 기준인가, 경계 기준인가?
결론: transform.position은 항상 오브젝트의 중심(정확히는 피봇, Pivot) 좌표를 기준으로 합니다.
- 지금 Player 오브젝트의 좌표는 이미지에서 보이는 것처럼 몸의 정중앙입니다.
- 따라서 플레이어의 x좌표가 맵의 왼쪽 끝(minX)에 도달하면, 캐릭터의 몸 절반이 이미 벽을 뚫고 나간 상태가 됩니다.
- 이걸 해결하려면 캐릭터의 넓이 절반만큼을 계산해서 더하거나 빼줘야 하지만, 지금 단계에서는 일단 중심 좌표 기준으로만 하셔도 전혀 문제없습니다. 먼저 큰 틀을 잡는 게 중요하니까요!
2. 작성하신 코드 분석 및 문제점
작성하신 코드는 "이렇게 하면 되지 않을까?" 라고 생각할 수 있는 아주 논리적인 접근입니다. 하지만 아쉽게도 몇 가지 이유로 의도대로 동작하지 않습니다.
- 가장 큰 문제: movement.x는 플레이어의 **'위치'**가 아니라, 조이스틱을 얼마나 기울였는지를 나타내는 **'-1 ~ 1 사이의 방향 값'**입니다.
- 따라서 if (movement.x <= cameraFollow.minX ...) 코드는 **'왼쪽으로 가려는 의지'(-1)와 '맵의 왼쪽 끝 좌표'(-10)**를 비교하는 것과 같아서, 논리적으로 맞지 않습니다.
3. 가장 쉽고 확실한 해결책 2가지
플레이어가 경계를 못 넘어가게 하는 방법은 크게 2가지가 있습니다. 지금 단계에서는 1번 방법을 강력하게 추천합니다.
방법 1: '보이지 않는 벽' 만들기 (Collider 활용) - (가장 추천!)
코드를 한 줄도 건드리지 않고, 물리적인 벽을 세워서 막는 가장 직관적인 방법입니다.
- 빈 오브젝트 생성: InGameScene의 하이어라키 창에서 우클릭 > Create Empty를 선택합니다. 이름은 Walls라고 지어주세요. (벽들을 정리해둘 폴더 역할)
- 왼쪽 벽 만들기:
- Walls 오브젝트를 우클릭 > Create Empty를 선택하고, 이름은 LeftWall로 합니다.
- LeftWall을 선택하고, 인스펙터 창에서 [Add Component] > **Box Collider 2D**를 추가합니다.
- Scene 뷰에서 LeftWall의 Box Collider 2D의 크기(Edit Collider 버튼)와 위치를 조절해서, 화면의 왼쪽에 길고 얇은 벽처럼 배치합니다.
- 나머지 벽 만들기: LeftWall을 복제(Ctrl+D)해서 RightWall, TopWall, BottomWall을 만들고, 각각 오른쪽, 위, 아래 경계에 배치합니다.
결과: 이제 플레이어 오브젝트에 있는 Rigidbody 2D와 Box Collider 2D가 이 보이지 않는 벽의 Box Collider 2D와 물리적으로 충돌하여 더 이상 나아가지 못하게 됩니다. 이게 가장 게임 엔진다운 해결 방법입니다.
(이해를 돕기 위한 예시 이미지)
방법 2: '좌표 강제 고정'하기 (코드 수정)
카메라 스크립트처럼, 플레이어의 위치를 강제로 경계 안에 머물도록 코드를 수정하는 방법입니다.
PlayerController.cs 스크립트의 FixedUpdate()를 아래와 같이 수정하세요. Update()는 원래대로 돌려놓으시면 됩니다.
// PlayerController.cs
// ... (다른 변수들은 그대로) ...
public CameraFollow cameraFollow; // 카메라의 경계값을 가져오기 위해 연결
// ... (Awake, Update 함수는 원래대로) ...
void FixedUpdate()
{
// 1. 먼저 속도에 따라 플레이어를 이동시킵니다.
rb.velocity = movement.normalized * moveSpeed;
// 2. 이동한 후의 위치가 경계를 벗어났는지 체크하고 강제로 되돌립니다.
// Mathf.Clamp(현재값, 최소값, 최대값) -> 현재값이 최소/최대 범위를 벗어나지 않도록 보정
float clampedX = Mathf.Clamp(rb.position.x, cameraFollow.minX, cameraFollow.maxX);
float clampedY = Mathf.Clamp(rb.position.y, cameraFollow.minY, cameraFollow.maxY);
// 3. 보정된 위치로 플레이어의 위치를 최종 설정합니다.
rb.position = new Vector2(clampedX, clampedY);
}
설정: 이 코드를 사용하려면 Player 오브젝트의 인스펙터 창에 있는 Player Controller 스크립트의 Camera Follow 슬롯에, 하이어라키 창의 Main Camera 오브젝트를 끌어다 놓아야 합니다.
결론 및 추천
두 방법 모두 장단점이 있지만, 지금은 '보이지 않는 벽(콜라이더)' 방법을 사용하시는 것을 가장 추천합니다.
- 이유: 코드가 복잡해지지 않고, 눈으로 보면서 벽을 배치할 수 있어 훨씬 직관적입니다. 또한 유니티의 물리 엔진과 콜라이더 시스템이 어떻게 동작하는지 자연스럽게 배울 수 있는 좋은 기회입니다.
간단한 비유: 영화 필름 vs 메트로놈
- Update()는 '영화 필름'과 같습니다.
- 컴퓨터 성능(FPS, Frames Per Second)에 따라 매 프레임마다 한 번씩 호출됩니다.
- 빠른 컴퓨터(예: 120 FPS)에서는 1초에 120번 호출됩니다.
- 느린 컴퓨터(예: 30 FPS)에서는 1초에 30번 호출됩니다.
- 즉, 호출되는 시간 간격이 항상 바뀝니다.
- FixedUpdate()는 '메트로놈'과 같습니다.
- 컴퓨터 성능과 상관없이 항상 고정된 시간 간격으로 호출됩니다.
- 기본 설정은 0.02초에 한 번 (즉, 1초에 50번)으로 정해져 있습니다. 이 값은 유니티 설정(Project Settings > Time > Fixed Timestep)에서 바꿀 수 있습니다.
- "똑...딱...똑...딱..." 하고 일정한 박자로 항상 정확하게 호출됩니다.
왜 두 개로 나누어져 있을까요? 바로 '물리 엔진' 때문입니다.
물리 계산(충돌, 속도, 힘, 중력 등)은 매우 정밀해야 합니다.
만약 Update()에서 물리 계산을 한다고 상상해보세요.
- 빠른 컴퓨터에서는 1초에 120번 계산하니까 물체가 부드럽고 정확하게 움직입니다.
- 느린 컴퓨터에서는 1초에 30번만 계산하니까, 계산 사이의 간격이 너무 길어져서 물체가 순간이동하거나 벽을 뚫어버리는 등의 예측 불가능한 오류가 발생할 수 있습니다.
따라서 유니티는 "모든 물리 관련 계산은 성능과 상관없이 항상 일정한 시간 간격으로 처리하겠다!"고 약속했고, 그 약속된 시간이 바로 FixedUpdate()입니다.
그래서 우리는 어떻게 사용해야 할까요? (가장 중요!)
아주 간단한 규칙이 있습니다.
- FixedUpdate()를 사용해야 할 때:
- Rigidbody의 속도(velocity)나 힘(AddForce)을 조절하는 모든 코드
- 즉, 물리적인 움직임과 관련된 모든 것은 여기에 넣어야 합니다.
- Update()를 사용해야 할 때:
- 사용자의 입력 받기 (Input.GetKey, Input.GetAxisRaw 등). 입력은 매 프레임 확인해야 놓치지 않습니다.
- 시간의 흐름에 따른 처리 (타이머, 쿨타임 등)
- UI 업데이트
- 애니메이션 상태 변경 등 물리와 직접적인 관련이 없는 대부분의 로직
"카메라의 이동 경계와 플레이어의 이동 경계를 코드로 분리하여, 카메라가 멈춘 뒤에도 플레이어는 화면 끝까지 이동할 수 있게 한다."
3단계: 유니티 에디터에서 설정하기 (매우 중요!)
- 스크립트 코드 교체: CameraFollow.cs와 PlayerController.cs를 위 코드로 각각 교체하고 저장합니다.
- 카메라 경계 설정:
- Main Camera를 선택합니다. 인스펙터 창을 보면 Camera Follow 스크립트의 변수들이 Camera Bounds와 Player Bounds로 바뀌었을 겁니다.
- Bounds는 **Center(중심점)**와 **Size(크기)**로 설정합니다.
- Camera Bounds: Center는 (0, 0, 0)으로 두고, Size를 (X: 20, Y: 10, Z: 0) 처럼 설정해보세요. (Scene 뷰에 노란색 사각형이 나타납니다)
- Player Bounds: Center는 (0, 0, 0)으로 두고, Size를 카메라 경계보다 더 크게 (X: 25, Y: 15, Z: 0) 처럼 설정합니다. (Scene 뷰에 빨간색 사각형이 나타납니다)
- 스크립트 연결 확인:
- Player 오브젝트를 선택합니다.
- Player Controller 스크립트의 Camera Follow 빈 슬롯에, 하이어라키 창의 Main Camera 오브젝트를 끌어다 놓습니다.
결과
이제 게임을 실행하면,
- 카메라는 노란색 경계(cameraBounds) 안에서만 움직입니다.
- 플레이어는 더 넓은 빨간색 경계(playerBounds) 안에서 움직일 수 있습니다.
- 따라서 카메라가 경계에 닿아 멈추더라도, 플레이어는 빨간색 경계에 닿을 때까지 화면 가장자리로 계속 이동할 수 있게 됩니다.

'개발 > 유니티' 카테고리의 다른 글
| 유니티, 기즈모 불편,크기 줄이기 (0) | 2025.09.07 |
|---|---|
| 유니티의, UI 버튼이 너무 클 때 (0) | 2025.09.07 |
| [프리다이빙 어플] 출시 (onestore에 게시되면 글 수정) (1) | 2025.09.06 |
| [습과어플]#7 (커밋해둘걸..) (0) | 2025.09.04 |
| 25.09.03(유니티) (0) | 2025.09.03 |