Unity 2D에서 스페이스바로 점프하는 공룡과 흘러가는 땅·구름 배경을 구현하는 러너 스타일 게임을 만들어봅니다. Rigidbody2D 점프, Physics2D.OverlapCircle을 이용한 바닥 감지, Animator 연동, 텍스처 오프셋과 오브젝트 이동을 이용한 스크롤 등을 학습합니다.
게임 구조
이 게임은 두 가지 핵심 스크립트로 구성됩니다:
| 스크립트 | 역할 |
|---|---|
DinoController |
공룡 점프 입력, 바닥 감지, Rigidbody2D·Animator 연동 |
Scroll |
땅(Ground) 텍스처 스크롤, 구름(Cloud) 왼쪽 이동·재등장 |
프로젝트 폴더 구조
content/sources/Assets/
├── 01.Scenes/ # 게임 씬
│ └── GameScene.unity
├── 02.Scripts/ # C# 스크립트
│ ├── DinoController.cs
│ └── Scroll.cs
├── 03.Images/ # 스프라이트·텍스처
│ ├── Dino_Run_1.png, Dino_Jump.png, Dino_Down.png 등
│ ├── Cactus_1~4.png, Pterosaur_1~2.png, Cloud.png, Ground.png
│ └── Materials/
│ └── Ground.mat
├── 04.Sounds/ # 효과음 (선택)
└── 05.Animations/ # Animator·Animation
├── Dino.controller
├── Dino_Run.anim, Dino_Jump.anim, Dino_Down.anim, Dino_Die.anim
└── ...
DinoController - 공룡 점프 컨트롤러
공룡 캐릭터의 점프와 바닥 감지, 애니메이션 전환을 담당하는 스크립트입니다.
전체 코드
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace DinoRun2D
{
/// <summary>
/// 공룡 캐릭터의 점프를 제어하는 스크립트.
/// 스페이스바를 누르면 공룡이 점프하고, 바닥에 착지하면 다시 점프할 수 있다.
/// </summary>
public class DinoController : MonoBehaviour
{
/// <summary>점프할 때 위로 가해지는 힘의 크기</summary>
[SerializeField] private float jumpForce;
/// <summary>현재 공룡이 바닥에 닿아있는지 여부</summary>
[SerializeField] private bool isGrounded;
/// <summary>바닥 감지 기준점 (공룡 발 아래에 위치시킨다)</summary>
[SerializeField] private Transform groundCheckPoint;
/// <summary>바닥으로 인식할 레이어 (Inspector에서 Ground 레이어 지정)</summary>
[SerializeField] private LayerMask whatIsGround;
/// <summary>바닥 감지 원형 영역의 반지름 (캐릭터 크기에 맞게 조절)</summary>
[SerializeField] private float groundCheckRadius = 0.2f;
/// <summary>달리기/점프 애니메이션 전환용 애니메이터</summary>
private Animator anim;
/// <summary>점프 시 물리 이동을 위한 Rigidbody2D</summary>
private Rigidbody2D rb;
/// <summary>애니메이터 파라미터 해시 캐싱 (문자열 비교보다 빠르고 오타 방지)</summary>
private static readonly int IsGroundHash = Animator.StringToHash("isGround");
void Start()
{
rb = GetComponent<Rigidbody2D>();
anim = GetComponent<Animator>();
anim.SetBool(IsGroundHash, true);
}
void Update()
{
isGrounded = Physics2D.OverlapCircle(groundCheckPoint.position, groundCheckRadius, whatIsGround);
if (Input.GetKeyDown(KeyCode.Space) && isGrounded)
{
rb.velocity = new Vector2(rb.velocity.x, jumpForce);
}
anim.SetBool(IsGroundHash, isGrounded);
}
void OnDrawGizmos()
{
if (groundCheckPoint == null) return;
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(groundCheckPoint.position, groundCheckRadius);
}
}
}
핵심 개념 설명
1. 바닥 감지 - Physics2D.OverlapCircle
isGrounded = Physics2D.OverlapCircle(groundCheckPoint.position, groundCheckRadius, whatIsGround);
- OverlapCircle: 2D 월드에서 지정한 위치에 원형 영역을 그려, 해당 레이어와 겹치는 Collider2D가 있으면
true를 반환(return)합니다. - groundCheckPoint: 공룡 발 아래에 빈 GameObject를 두고 Transform으로 할당하면, 그 위치를 중심으로 감지합니다.
- whatIsGround: LayerMask로 "Ground" 레이어만 감지하도록 설정하면, 장애물·트리거 등과 구분할 수 있습니다.
이렇게 하면 "바닥에만 있을 때만 점프"가 가능해져서 이중 점프를 막을 수 있습니다.
2. 점프 처리 - velocity 유지
if (Input.GetKeyDown(KeyCode.Space) && isGrounded)
{
rb.velocity = new Vector2(rb.velocity.x, jumpForce);
}
- GetKeyDown(KeyCode.Space): 스페이스바를 누른 그 프레임에만 true.
- velocity: Rigidbody2D의 현재 속도. x는 달리기 스크롤 등으로 이미 움직일 수 있으므로 유지하고, y만
jumpForce로 설정해 위로 튀어오르게 합니다. - AddForce 대신 velocity: 즉시 속도를 지정하는 방식이라 반응이 명확하고, 점프 높이를 숫자 하나로 조절하기 쉽습니다.
3. Animator 파라미터 - 해시 사용
private static readonly int IsGroundHash = Animator.StringToHash("isGround");
// ...
anim.SetBool(IsGroundHash, isGrounded);
- StringToHash("isGround"): 문자열(string) "isGround"를 정수(integer) 해시로 바꿉니다.
- SetBool(해시, 값): 매 프레임 문자열(string) 비교 대신 해시로 파라미터를 찾아 설정하므로 성능에 유리하고, 오타로 인한 런타임 오류를 줄일 수 있습니다.
- Animator에 bool 파라미터
isGround를 만들고, Transition 조건으로 "달리기 ↔ 점프" 전환을 설정하면 됩니다.
4. Gizmos로 디버깅
void OnDrawGizmos()
{
if (groundCheckPoint == null) return;
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(groundCheckPoint.position, groundCheckRadius);
}
- OnDrawGizmos: 에디터 Scene 뷰에만 그려지며, 게임 실행과 무관하게 항상 호출됩니다.
- DrawWireSphere: 바닥 감지 반경을 빨간 원으로 표시해, 반지름과 위치를 눈으로 확인하기 쉽습니다.
Scroll - 배경 스크롤
땅은 텍스처가 흘러가고, 구름은 오브젝트가 왼쪽으로 이동하다가 화면 밖으로 나가면 오른쪽에서 다시 등장하도록 합니다.
전체 코드
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace DinoRun2D
{
/// <summary>
/// 배경 스크롤을 담당하는 스크립트.
/// - 땅(Ground): 텍스처 오프셋을 이동시켜 땅이 흘러가는 효과를 만든다.
/// - 구름(Cloud): 오브젝트 자체를 왼쪽으로 이동시키고, 화면 밖으로 나가면 오른쪽에서 다시 등장한다.
/// </summary>
public class Scroll : MonoBehaviour
{
[SerializeField] private float groundScrollSpeedX = 2f;
private float cloudScrollSpeedX;
private Renderer quadRenderer;
[SerializeField] private bool isCloud;
[SerializeField] private float leftBoundary = -11f;
[SerializeField] private float rightBoundary = 11f;
[SerializeField] private float cloudMinY = 0.5f;
[SerializeField] private float cloudMaxY = 4f;
private Material materialInstance;
void Start()
{
quadRenderer = GetComponent<Renderer>();
if (isCloud)
{
cloudScrollSpeedX = Random.Range(0.05f, 0.09f);
}
else
{
materialInstance = quadRenderer.material;
}
}
void Update()
{
if (isCloud)
{
transform.position = new Vector3(
transform.position.x - cloudScrollSpeedX * Time.deltaTime,
transform.position.y,
transform.position.z
);
if (transform.position.x <= leftBoundary)
{
transform.position = new Vector3(rightBoundary, Random.Range(cloudMinY, cloudMaxY), 0f);
}
}
else
{
float offsetX = Time.time * groundScrollSpeedX;
materialInstance.mainTextureOffset = new Vector2(offsetX, 0);
}
}
void OnDestroy()
{
if (materialInstance != null)
{
Destroy(materialInstance);
}
}
}
}
핵심 개념 설명
1. 땅 스크롤 - 텍스처 오프셋
float offsetX = Time.time * groundScrollSpeedX;
materialInstance.mainTextureOffset = new Vector2(offsetX, 0);
- mainTextureOffset: Material이 사용하는 메인 텍스처의 UV 오프셋입니다. 값을 바꾸면 텍스처가 이동한 것처럼 보입니다.
- Time.time: 게임 시작 후 경과 시간(초)이므로,
groundScrollSpeedX만큼 초당 오프셋이 증가해 땅이 끊임없이 흘러가는 착시가 생깁니다. - materialInstance:
quadRenderer.material은 접근할 때마다 Material 인스턴스(instance)가 복제될 수 있으므로, Start에서 한 번만 캐싱해 사용하고 OnDestroy에서 Destroy하여 메모리(memory) 누수를 막습니다.
2. 구름 스크롤 - 오브젝트 이동·재등장
transform.position = new Vector3(
transform.position.x - cloudScrollSpeedX * Time.deltaTime,
transform.position.y,
transform.position.z
);
if (transform.position.x <= leftBoundary)
{
transform.position = new Vector3(rightBoundary, Random.Range(cloudMinY, cloudMaxY), 0f);
}
- Time.deltaTime: 프레임 간 간격(초)이므로, 속도를 곱해 주면 기기 성능에 관계없이 일정한 속도로 이동합니다.
- leftBoundary / rightBoundary: 화면 왼쪽 밖(
leftBoundary이하)으로 나가면 오른쪽(rightBoundary)으로 순간이동하고, Y는Random.Range(cloudMinY, cloudMaxY)로 바꿔 매번 다른 높이에 나타나게 할 수 있습니다. - 구름마다 다른 속도: Start에서
cloudScrollSpeedX = Random.Range(0.05f, 0.09f)로 두면 구름마다 속도가 달라 원근감을 줄 수 있습니다.
3. Material 인스턴스(instance) 정리
void OnDestroy()
{
if (materialInstance != null)
{
Destroy(materialInstance);
}
}
- 런타임에
renderer.material로 접근하면 Unity가 Material을 복제합니다. 이 복제본을 오브젝트가 파괴될 때 함께 Destroy하지 않으면 메모리(memory) 누수가 발생할 수 있으므로, 캐싱해 둔materialInstance만 OnDestroy에서 정리합니다.
Unity 씬 설정 요약
| 항목 | 권장 설정 |
|---|---|
| 공룡 | Rigidbody2D (Gravity Scale 적절히), Collider2D, Animator, DinoController. groundCheckPoint는 발 아래 빈 오브젝트 |
| 바닥(땅) | Quad 등에 Ground 텍스처·Material, Scroll 스크립트 (isCloud = false). 필요 시 "Ground" 레이어 지정 |
| 구름 | Sprite/Quad에 Scroll 스크립트 (isCloud = true), leftBoundary/rightBoundary로 화면 밖 경계 설정 |
| 레이어 | 바닥용 "Ground" 레이어 생성 후, DinoController의 whatIsGround에 지정 |
이 구성을 적용하면 스페이스바 점프와 배경 스크롤이 동작하는 공룡 달리기 형태의 2D 러너 기반을 만들 수 있습니다. 장애물 생성·충돌·점수·게임 오버는 같은 시리즈의 다른 튜토리얼(오브젝트 생성, 2D 충돌 감지 등)을 조합해 확장할 수 있습니다.